├── .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 | | | ![\|50](_test.png) [^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;} --------------------------------------------------------------------------------