├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.yml
└── workflows
│ └── nodejs-build.yml
├── .gitignore
├── LICENSE
├── README.md
├── esbuild.config.mjs
├── main_tobuild.js
├── manifest.json
├── package.json
├── src
├── lazyParsers
│ ├── FootMgr.js
│ ├── SheetRender.js
│ ├── SheetRender.old.md
│ ├── blockParser.js
│ ├── buildContGrid.js
│ └── postParser.js
├── mergeTable
│ ├── handleFocus.js
│ └── mergeTable.js
├── sheet.js
└── utils.js
└── styles.css
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: Report a bug
3 | body:
4 | - type: checkboxes
5 | attributes:
6 | label: Check before the Report
7 | description: |
8 | If you know Chinese:
9 |
如果你懂中文,请参照 [故障排查指导 Troubleshooting Guide](https://forum-zh.obsidian.md/t/topic/27879/1) 先行排查。
10 |
else, you can refer to [About the Bug reports category - Obsidian Forum](https://forum.obsidian.md/t/about-the-bug-reports-category/24/11)
11 |
*On desktop, open the sandbox vault (Open Help > Sandbox Vault, this can be accessed from the command palette or the lower left ribbon) and see if you can reproduce the issue.*
12 | options:
13 | - label: I have verified that I am on the latest version of the plugin.
14 | - label: I have tested in Sandbox Vault.
15 | required: true
16 |
17 | - type: textarea
18 | attributes:
19 | label: Steps
20 | description: If applicable, add screenshots, GIFs and attachments to help explain your problem.
21 | value: |
22 | **Describe the Bug**
23 |
24 | **How to Reproduce**
25 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs-build.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI/CD
2 | run-name: ${{ inputs.name }}
3 |
4 | on:
5 | workflow_dispatch:
6 | inputs:
7 | name:
8 | required: true
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - run: npm i
16 | - run: npm run build
17 |
18 | - name: Rename main1 to main
19 | run: |
20 | if [ -f main1.js ]; then
21 | mv main1.js main.js
22 | fi
23 |
24 | - name: Upload artifact
25 | uses: actions/upload-artifact@v4
26 | with:
27 | name: sheets-basic
28 | path: |
29 | main.js
30 | manifest.json
31 | styles.css
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | main.js
3 | main1.js
4 | git-*.bat
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2025, PlayerMiller109
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sheets Basic
2 |
3 | 中文介绍见 [Obsidian Chinese Forum t35091](https://forum-zh.obsidian.md/t/topic/35091/1)
4 |
5 | - Use the plugin command 'rebuildCurrent' (default hotkey `F5`) if you encounter any problems.
6 | - When used in a merged table cell, it will unmerge the cell, and the cell will become a normal cell.
7 | - When used in a normal table cell, it will refresh the table. Avoid placing your cursor in a signifier cell.
8 | - When used outside tables, it will refresh the active leaf.
9 | - Switch to reading mode before exporting a PDF. It is recommended to refresh once before the export.
10 | - Because Obsidian has a reading mode cache, and no method is provided to precisely clear the cache currently.
11 | - The same applies before starting a slide presentation.
12 | - Do not use the up Sign in the first row of the table body; that is, do not merge the table header and body.
13 |
14 |
15 | Test text, click to unfold
16 |
17 | ````markdown
18 | | head1 | < |
19 | | ----- | ----------------------- |
20 | | |  [^1] |
21 | | | ^ |
22 |
23 | [^1]: footnote1
24 |
25 | > | head2 | < |
26 | > | ----- | ------ |
27 | > | | table2 |
28 | > | | ^ |
29 | >
30 | > | head3 | < |
31 | > | ----- | ------ |
32 | > | | table3 |
33 | > | | ^ |
34 |
35 | > [!quote]
36 | > | head4 | < |
37 | > | ----- | ------ |
38 | > | | table4 |
39 | > | | ^ |
40 | >
41 | > | head5 | < |
42 | > | ----- | ------ |
43 | > | | table5 |
44 | > | | ^ |
45 |
46 | ```sheet
47 | | head6 | < |
48 | | ----- | ------ |
49 | | | table6 |
50 | | | ^ |
51 | ```
52 | ````
53 |
54 |
55 | (2025-04-22) Test in Obsidian v1.8.10 Sandbox Vault. left: Live Preview; right: Reading Mode:
56 |
57 |
58 |
59 | ## For Developers
60 |
61 | 不推荐二次开发,因为官方不赞成主要功能参涉私有接口,这样做不划算。
62 |
63 | It is not recommended for secondary development because the official does not advocate that the main functions involve private APIs of Obsidian. It's not cost-effective.
64 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild';
2 | import process from 'process';
3 | import builtins from 'builtin-modules'
4 |
5 | const banner = '/* Esbuild-built. Source on GitHub. */';
6 |
7 | const prod = (process.argv[2] === 'production');
8 |
9 | const context = await esbuild.context({
10 | entryPoints: ['main_tobuild.js'],
11 | outfile: 'main1.js',
12 | bundle: true,
13 | minify: true,
14 | treeShaking: true,
15 | sourcemap: prod ? false : 'inline',
16 | format: 'cjs',
17 | target: 'esnext',
18 | external: [
19 | 'obsidian',
20 | 'electron',
21 | '@codemirror/autocomplete',
22 | '@codemirror/collab',
23 | '@codemirror/commands',
24 | '@codemirror/language',
25 | '@codemirror/lint',
26 | '@codemirror/search',
27 | '@codemirror/state',
28 | '@codemirror/view',
29 | '@lezer/common',
30 | '@lezer/highlight',
31 | '@lezer/lr',
32 | ...builtins,
33 | ],
34 | banner: {
35 | js: banner,
36 | },
37 | });
38 |
39 | if (prod) {
40 | await context.rebuild();
41 | process.exit(0);
42 | } else {
43 | await context.watch();
44 | }
--------------------------------------------------------------------------------
/main_tobuild.js:
--------------------------------------------------------------------------------
1 | const ob = require('obsidian'), { ViewPlugin } = require('@codemirror/view')
2 | module.exports = class extends ob.Plugin {
3 | onload() {
4 | const sheet = require('./src/sheet.js')(this.app, {ob, ViewPlugin})
5 | sheet.call(this)
6 | }
7 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "sheets-basic",
3 | "name": "Sheets Basic",
4 | "version": "0.0.1",
5 | "minAppVersion": "1.5.0",
6 | "description": "Merge markdown table cells.",
7 | "author": "PlayerMiller109",
8 | "authorUrl": "https://github.com/PlayerMiller109",
9 | "isDesktopOnly": false
10 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dev": "node esbuild.config.mjs",
4 | "build": "node esbuild.config.mjs production"
5 | },
6 | "author": "PlayerMiller109",
7 | "devDependencies": {
8 | "builtin-modules": "^4.0.0",
9 | "esbuild": "^0.24.2",
10 | "obsidian": "latest"
11 | }
12 | }
--------------------------------------------------------------------------------
/src/lazyParsers/FootMgr.js:
--------------------------------------------------------------------------------
1 | module.exports = class {
2 | inlineRefs = []; normalRefs = []
3 | retain = (el)=> {
4 | const footEls = el.querySelectorAll('.footnote-ref')
5 | Array.from(footEls).map(el=> {
6 | const ref = ({
7 | cont: el.children[0].dataset.footref,
8 | html: el.outerHTML, el,
9 | })
10 | if (el.querySelector(':scope > '+this.inlineAttr)) {
11 | this.inlineRefs.push(ref)
12 | }
13 | else this.normalRefs.push(ref)
14 | })
15 | }
16 | inlineAttr = '[data-footref^="[inline"]'
17 | footRE = /(? {
19 | return text.replaceAll(this.footRE, `↿$1↿`)
20 | }
21 | restore = (text)=> {
22 | return text.replaceAll(
23 | /↿(.+?)↿/g, (m, p1)=> {
24 | const ref = this.normalRefs.find(ref=> ref.cont === p1)
25 | return ref ? ref.html : p1
26 | }
27 | )
28 | }
29 | handleInline = (cellEl)=> {
30 | const footsSec = cellEl.querySelector('section.footnotes')
31 | if (footsSec) {
32 | footsSec.remove()
33 | cellEl.querySelectorAll(this.inlineAttr).forEach((child, i)=> {
34 | child.parentElement.replaceWith(this.inlineRefs[i].el)
35 | })
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/src/lazyParsers/SheetRender.js:
--------------------------------------------------------------------------------
1 | const { tableId, merge } = require('../utils.js')
2 | const FootMgr = require('./FootMgr.js')
3 | module.exports = (app, ob)=> class extends ob.MarkdownRenderChild {
4 | constructor(el, contGrid) {
5 | super(el)
6 | const tableEl = el
7 | tableEl.id = tableId
8 | this.domGrid = Array.from(tableEl.rows).map((tr, rowIndex)=> {
9 | return Array.from(tr.cells).map((td, colIndex)=> ({
10 | el: td, row: rowIndex, col: colIndex,
11 | text: contGrid[rowIndex][colIndex],
12 | }))
13 | })
14 | this.buildDomTable()
15 | }
16 | onload() {}
17 | onunload() {}
18 |
19 | buildDomTable() {
20 | const cells = this.domGrid.flat()
21 | for (const _cell of cells) {
22 | const merged = merge(_cell, cells)
23 | if (!merged) this.normalizeCell(_cell)
24 | }
25 | }
26 | normalizeCell({text, el}) {
27 | const footMgr = new FootMgr()
28 | text = text.replaceAll('
', '\n')
29 | text = footMgr.dummy(text)
30 | footMgr.retain(el)
31 | el.empty()
32 | ob.MarkdownRenderer.render(
33 | app, text||'\u200B', el, '', this
34 | ).then(()=> this.afterRender(el, footMgr))
35 | }
36 | afterRender = (cellEl, footMgr)=> {
37 | footMgr.handleInline(cellEl)
38 | const isP = el=> el.tagName == 'P';
39 | [cellEl.firstChild, cellEl.lastChild].map(el=> {
40 | if (!isP(el)) return
41 | if (!el.textContent && !el.children[0])
42 | el.remove()
43 | })
44 | let _ihtml = ''
45 | for (const node of cellEl.childNodes) {
46 | if (node.nodeType === 3) {
47 | _ihtml += (node.data === '\n') ? '
' : node.data
48 | }
49 | else _ihtml += isP(node) ? node.innerHTML : node.outerHTML
50 | }
51 | _ihtml = footMgr.restore(_ihtml)
52 | cellEl.innerHTML = _ihtml
53 | }
54 | }
--------------------------------------------------------------------------------
/src/lazyParsers/SheetRender.old.md:
--------------------------------------------------------------------------------
1 |
2 | ```js
3 | this.tableHead = tableEl.createEl('thead')
4 | this.tableBody = tableEl.createEl('tbody')
5 | ```
6 |
7 | ```js
8 | if (this.headerRow !== -1) {
9 | const heads = this.contGrid[this.headerRow]
10 | this.colStyles = this.getHeaderStyles(heads)
11 | }
12 | getHeaderStyles(heads) {
13 | return heads.map(head=> {
14 | const alignment = head.match(this.headerRE), styles = {}
15 | if (alignment[1] && alignment[2]) styles['textAlign'] = 'center';
16 | else if (alignment[1]) styles['textAlign'] = 'left';
17 | else if (alignment[2]) styles['textAlign'] = 'right';
18 | return { styles }
19 | })
20 | }
21 | ```
22 |
--------------------------------------------------------------------------------
/src/lazyParsers/blockParser.js:
--------------------------------------------------------------------------------
1 | const { buildContGrid } = require('./buildContGrid.js')
2 | module.exports = (app, ob)=> {
3 | const SheetRender = require('./SheetRender.js')(app, ob)
4 | return async (source, el, ctx)=> {
5 | await ob.MarkdownRenderer.render(app, source, el, '', this)
6 | const tEl = el.querySelector('table'); if (!tEl) return
7 | const grid = buildContGrid(source.split('\n'))
8 | if (grid) ctx.addChild(new SheetRender(tEl, grid))
9 | }
10 | }
--------------------------------------------------------------------------------
/src/lazyParsers/buildContGrid.js:
--------------------------------------------------------------------------------
1 | const fI = (arr, isPipeStart)=> arr.findIndex(
2 | i=> (isPipeStart ? /^\|/ : /^(?!\|)/).test(i)
3 | )
4 | module.exports = new class {
5 | borderRE = /(? line.replace(/^.*?(?=(? {
10 | if (rowSources[0].startsWith('```')) return // exclude codeblock
11 | rowSources.splice(0, fI(rowSources, !0))
12 | let endIndex = fI(rowSources), contGrid
13 | while (endIndex > -1) {
14 | const expected = rowSources.splice(0, endIndex)
15 | contGrid = this.buildContGrid(expected)
16 | if (!contGrid) {
17 | rowSources.splice(0, fI(rowSources, !0))
18 | endIndex = fI(rowSources)
19 | continue
20 | }
21 | prev.r = rowSources
22 | return contGrid
23 | }
24 | if (endIndex === -1) {
25 | return this.buildContGrid(rowSources)
26 | }
27 | }
28 | buildContGrid = (sources)=> {
29 | const contGrid = sources.filter(row=> row).map(
30 | row=> row.split(this.borderRE).slice(1, -1).map(cell=> cell.trim())
31 | )
32 | const headerRow = contGrid.findIndex(
33 | row=> row.every(col=> this.headerRE.test(col))
34 | )
35 | if (contGrid[headerRow-1] && contGrid[headerRow+1]) {
36 | contGrid.splice(headerRow, 1)
37 | return contGrid
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/lazyParsers/postParser.js:
--------------------------------------------------------------------------------
1 | const { trimLeading, rgxFindTable } = require('./buildContGrid.js')
2 | module.exports = (app, ob)=> {
3 | const SheetRender = require('./SheetRender.js')(app, ob)
4 | return postParser = new class {
5 | grid = []
6 | main = (el, ctx)=> {
7 | if (el.hasClass('block-language-sheet')) return
8 | const view5 = app.workspace.getActiveFileView(); if (!view5) return
9 | const tableEls = Array.from(el.querySelectorAll('table')); if (!tableEls[0]) return
10 | const prev = {}
11 | tableEls.map(async (tEl, tIndex)=> {
12 | let grid; const sec = ctx.getSectionInfo(tEl)
13 | if (!sec) {
14 | await sleep(50)
15 | const callout = tEl.offsetParent
16 | if (callout?.cmView) { // for source mode, assume table is in callout
17 | let rowSources
18 | if (prev.callout === callout) rowSources = prev.r;
19 | else {
20 | const a1 = callout.cmView.widget.text; if (!a1) return // table in Dataview
21 | rowSources = a1.split('\n').map(line=> trimLeading(line))
22 | }
23 | prev.callout = callout
24 | grid = rgxFindTable(prev, rowSources)
25 | }
26 | else {
27 | grid = this.grid[tIndex] // when export
28 | // if (grid && el.className == 'slides') return
29 | }
30 | }
31 | // reading mode
32 | else {
33 | const { text, lineStart, lineEnd } = sec; let rowSources
34 | if (
35 | prev.t == text && prev.s == lineStart && prev.ed == lineEnd
36 | ) rowSources = prev.r; // continue old one
37 | else {
38 | const a1 = text.split('\n').slice(lineStart, lineEnd+1)
39 | rowSources = a1.map(line=> trimLeading(line)) // get new one
40 | }
41 | prev.t = text; prev.s = lineStart; prev.ed = lineEnd
42 | grid = rgxFindTable(prev, rowSources)
43 | this.grid.push(grid)
44 | }
45 | if (grid) ctx.addChild(new SheetRender(tEl, grid))
46 | })
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/mergeTable/handleFocus.js:
--------------------------------------------------------------------------------
1 | const { isSign } = require('../utils.js')
2 | const handleFocus = table=> {
3 | const _old = table.receiveCellFocus
4 | table.receiveCellFocus = function(row, col, func, flag) {
5 | if (table.rows[row]?.[col]?.el.style.display == 'none') {
6 | const { cell } = table.editor.tableCell
7 | , { row: maxRow, col: maxCol } = table.rows.flat().pop()
8 | if (row === cell.row) {
9 | while (isSign(table.rows[row]?.[col]?.text))
10 | col += col < cell.col ? -1 : 1
11 | if (col < 0) {
12 | while (isSign(table.rows[row]?.[0].text)) row--
13 | }
14 | if (col > maxCol) {
15 | col = 0; row++
16 | if (row > maxRow) table.insertRow(row, col)
17 | }
18 | }
19 | else if (col === cell.col) {
20 | while (isSign(table.rows[row]?.[col]?.text))
21 | row += row < cell.row ? -1 : 1
22 | if (row < 0) {
23 | while (isSign(table.rows[0][col]?.text)) col--
24 | }
25 | }
26 | else {
27 | if (row === cell.row - 1) {
28 | while (isSign(table.rows[row][col]?.text)) col--
29 | }
30 | if (row === cell.row + 1) {
31 | while (isSign(table.rows[row][col]?.text)) col++
32 | }
33 | }
34 | }
35 | _old.call(this, row, col, func, flag)
36 | }
37 | }
38 | module.exports = handleFocus
--------------------------------------------------------------------------------
/src/mergeTable/mergeTable.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('../utils.js')
2 | const handleFocus = require('./handleFocus.js')
3 | const mergeTable = table=> {
4 | const cells = table.rows.flat()
5 | for (const _cell of cells) merge(_cell, cells)
6 | handleFocus(table)
7 | }
8 | const mergeAllInView = view=> view.docView.children.flatMap(c=>
9 | c.dom.className.includes('table-widget') ? c.widget : []
10 | ).map(mergeTable)
11 | module.exports = { mergeTable, mergeAllInView }
--------------------------------------------------------------------------------
/src/sheet.js:
--------------------------------------------------------------------------------
1 | const { mergeTable, mergeAllInView } = require('./mergeTable/mergeTable.js')
2 | module.exports = (app, {ob, ViewPlugin})=> {
3 | const getEMode = ()=> app.workspace.getActiveFileView()?.editMode
4 | class liveParser {
5 | update(update) {
6 | const eMode = getEMode(); if (!eMode) return
7 | const { tableCell } = eMode // when cursor in a table you can get tableCell
8 | const undo = update.transactions.find(tr=> tr.isUserEvent('undo'))
9 | // table.render() is an Ob prototype, you can use table.rebuildTable() too
10 | if (undo && tableCell) { tableCell.table.render(); mergeTable(tableCell.table) }
11 | const { view } = update
12 | if (
13 | update.focusChanged && view.hasFocus
14 | || update.viewportChanged
15 | ) setTimeout(()=> mergeAllInView(view))
16 | }
17 | }
18 | const postParser = require('./lazyParsers/postParser.js')(app, ob)
19 | const updateMerge = ()=> {
20 | postParser.source = []
21 | const eMode = getEMode(); if (!eMode) return
22 | const view = eMode.cm
23 | if (view) setTimeout(()=> mergeAllInView(view), 50)
24 | }
25 | const unmergeCell = tableCell=> {
26 | const { table, cell } = tableCell
27 | , cells = table.rows.flat(), { row, col, el: cellEl } = cell
28 | if (cellEl.rowSpan > 1 || cellEl.colSpan > 1) {
29 | cells.filter(cell2=>
30 | row <= cell2.row && cell2.row < row + cellEl.rowSpan
31 | && col <= cell2.col && cell2.col < col + cellEl.colSpan
32 | ).map(cell2=> {
33 | cell2.el.removeAttribute('id')
34 | cell2.el.style.display = 'table-cell'
35 | })
36 | cellEl.colSpan = cellEl.rowSpan = 1; return !0
37 | }
38 | }
39 | const blockParser = require('./lazyParsers/blockParser.js')(app, ob)
40 | return function() {
41 | this.registerMarkdownPostProcessor(postParser.main)
42 | this.registerMarkdownCodeBlockProcessor('sheet', blockParser)
43 | this.registerEvent(app.workspace.on('file-open', updateMerge))
44 | app.workspace.onLayoutReady(updateMerge)
45 | this.addCommand({
46 | id: 'rebuild', name: 'rebuildCurrent',
47 | callback: async ()=> {
48 | postParser.source = []
49 | const eMode = getEMode(); if (!eMode) return
50 | const { tableCell } = eMode
51 | if (tableCell) {
52 | const checking = unmergeCell(tableCell)
53 | if (!checking) mergeTable(tableCell.table)
54 | }
55 | else {
56 | const leaves = app.workspace.getLeavesOfType('markdown')
57 | .filter(leaf=> leaf.view.path == eMode.path)
58 | for (const leaf of leaves) await leaf.rebuildView()
59 | }
60 | },
61 | hotkeys: [{modifiers: [], key: 'F5'}]
62 | })
63 | this.registerEditorExtension([ViewPlugin.fromClass(liveParser)])
64 | }
65 | }
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const Sign = {up: '^', left: '<'}
2 | const cellId = 'm-sheet-cell'
3 | const disable = el=> { el.id = cellId; el.style.display = 'none' }
4 | module.exports = new class {
5 | tableId = 'm-sheet'
6 | isSign = text=> Object.values(Sign).includes(text)
7 | merge = (_cell, cells)=> {
8 | if (_cell.el.id == cellId) return
9 | let i = 1, params
10 | if (_cell.text == Sign.left && _cell.col > 0)
11 | params = {
12 | find: cell2=> cell2.row == _cell.row && cell2.col == _cell.col - i,
13 | stop: Sign.up, type: 'colSpan',
14 | }
15 | else if (_cell.text == Sign.up && _cell.row > 0)
16 | params = {
17 | find: cell2=> cell2.row == _cell.row - i && cell2.col == _cell.col,
18 | stop: Sign.left, type: 'rowSpan',
19 | }
20 | else return
21 | disable(_cell.el)
22 | const { find, stop, type } = params
23 | let cell, broke
24 | do {
25 | cell = cells.find(find)
26 | if (!cell || cell.text == stop) { broke = !0; break }
27 | i++
28 | } while (cell.el.id == cellId); if (broke) return
29 | const { el: cellEl } = cell
30 | cellEl[type] || Object.assgin(cellEl, { [type]: 1 })
31 | cellEl[type] += 1
32 | return !0
33 | }
34 | }
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | [id='m-sheet'] {--table-white-space: normal;}
--------------------------------------------------------------------------------