diff --git a/docs/dev/code/BLOCK_ADDITION_PROPERTY.md b/docs/dev/code/BLOCK_ADDITION_PROPERTY.md index 73479e70..7fa03ee3 100644 --- a/docs/dev/code/BLOCK_ADDITION_PROPERTY.md +++ b/docs/dev/code/BLOCK_ADDITION_PROPERTY.md @@ -8,6 +8,8 @@ - codeContent (used in code block) + - cellContent (used in table cell, it's parent must be th or td block) + - atxLine (can not contain soft line break and hard line break use in atx heading) - thematicBreakLine (can not contain soft line break and hard line break use in thematic break) diff --git a/src/main/menu/actions/paragraph.js b/src/main/menu/actions/paragraph.js index 41382c52..1968ad26 100644 --- a/src/main/menu/actions/paragraph.js +++ b/src/main/menu/actions/paragraph.js @@ -102,7 +102,7 @@ export const updateSelectionMenus = (applicationMenu, { start, end, affiliation setParagraphMenuItemStatus(applicationMenu, true) if ( - (/th|td/.test(start.type) && /th|td/.test(end.type)) || + (start.block.functionType === 'cellContent' && end.block.functionType === 'cellContent') || (start.type === 'span' && start.block.functionType === 'codeContent') || (end.type === 'span' && end.block.functionType === 'codeContent') ) { diff --git a/src/muya/lib/assets/pngicon/table/table.png b/src/muya/lib/assets/pngicon/table/table.png index d09de4f3..86851876 100755 Binary files a/src/muya/lib/assets/pngicon/table/table.png and b/src/muya/lib/assets/pngicon/table/table.png differ diff --git a/src/muya/lib/assets/pngicon/table/table@2x.png b/src/muya/lib/assets/pngicon/table/table@2x.png index 4b5db96a..4db95b15 100755 Binary files a/src/muya/lib/assets/pngicon/table/table@2x.png and b/src/muya/lib/assets/pngicon/table/table@2x.png differ diff --git a/src/muya/lib/assets/pngicon/table/table@3x.png b/src/muya/lib/assets/pngicon/table/table@3x.png index bf53a041..c23e6076 100755 Binary files a/src/muya/lib/assets/pngicon/table/table@3x.png and b/src/muya/lib/assets/pngicon/table/table@3x.png differ diff --git a/src/muya/lib/assets/pngicon/table_delete/1.png b/src/muya/lib/assets/pngicon/table_delete/1.png new file mode 100644 index 00000000..920e9726 Binary files /dev/null and b/src/muya/lib/assets/pngicon/table_delete/1.png differ diff --git a/src/muya/lib/assets/pngicon/table_delete/2.png b/src/muya/lib/assets/pngicon/table_delete/2.png new file mode 100644 index 00000000..f8b19d87 Binary files /dev/null and b/src/muya/lib/assets/pngicon/table_delete/2.png differ diff --git a/src/muya/lib/assets/pngicon/table_delete/3.png b/src/muya/lib/assets/pngicon/table_delete/3.png new file mode 100644 index 00000000..7aa5397e Binary files /dev/null and b/src/muya/lib/assets/pngicon/table_delete/3.png differ diff --git a/src/muya/lib/assets/styles/index.css b/src/muya/lib/assets/styles/index.css index 9507b83f..bc55002c 100644 --- a/src/muya/lib/assets/styles/index.css +++ b/src/muya/lib/assets/styles/index.css @@ -40,10 +40,18 @@ div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-paragraph-conte .ag-atx-line:empty::after, .ag-thematic-break-line:empty::after, .ag-code-content:empty::after, +.ag-cell-content:empty::after, .ag-paragraph-content:empty::after { content: '\200B'; } +.ag-cell-content { + display: inline-block; + min-width: 4em; + width: 100%; + min-height: 10px; +} + .ag-atx-line, .ag-thematic-break-line, .ag-paragraph-content, @@ -249,14 +257,15 @@ span.ag-math > .ag-math-render.ag-math-error { figure { padding: 0; margin: 0; - margin: 1rem 0; + margin: 1.4em 0; position: relative; } + .ag-tool-bar { width: 100%; user-select: none; position: absolute; - top: -20px; + top: -22px; left: 0; display: none; } @@ -273,7 +282,7 @@ figure { display: flex; width: 20px; height: 20px; - margin-right: 3px; + margin-right: 6px; cursor: pointer; border-radius: 3px; color: var(--iconColor); @@ -281,14 +290,12 @@ figure { align-items: center; } -.ag-tool-bar ul li[data-label=delete] { - position: absolute; - top: 0; +.ag-tool-bar ul li[data-label=table] { + margin-right: 16px; } .ag-tool-bar ul li[data-label=delete] { - color: var(--deleteColor); - right: 0; + margin-left: 16px; } .ag-container-icon { @@ -313,7 +320,7 @@ figure { width: 16px; overflow: hidden; color: var(--iconColor); - opacity: .5; + opacity: .7; transition: all .25s ease-in-out; } @@ -339,8 +346,7 @@ figure { } .ag-tool-bar ul li[data-label=delete] i.icon { - color: var(--deleteColor); - opacity: 1; + color: var(--iconColor); } figure.ag-active .ag-tool-bar { @@ -371,7 +377,6 @@ figure[data-role=HTML]:not(.ag-active):hover .ag-container-icon { } table { - width: 100%; border-collapse: collapse; margin-top: 0; } @@ -495,7 +500,6 @@ pre.ag-multiple-math span.ag-code-content:first-of-type:empty::after { color: var(--editorColor10); } -figure, pre.ag-html-block, pre.ag-fence-code, pre.ag-indent-code, diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js index aad1f0ee..09f26f7e 100644 --- a/src/muya/lib/config/index.js +++ b/src/muya/lib/config/index.js @@ -6,7 +6,7 @@ import voidHtmlTags from 'html-tags/void' // Electron 2.0.2 not support yet! So give a default value 4 export const DEVICE_MEMORY = navigator.deviceMemory || 4 // Get the divice memory number(Chrome >= 63) export const UNDO_DEPTH = DEVICE_MEMORY >= 4 ? 100 : 50 -export const HAS_TEXT_BLOCK_REG = /^(span|th|td)/i +export const HAS_TEXT_BLOCK_REG = /^span$/i export const VOID_HTML_TAGS = voidHtmlTags export const HTML_TAGS = htmlTags // TYPE1 ~ TYPE7 according to https://github.github.com/gfm/#html-blocks diff --git a/src/muya/lib/contentState/arrowCtrl.js b/src/muya/lib/contentState/arrowCtrl.js index 3197e0d8..71d8d7de 100644 --- a/src/muya/lib/contentState/arrowCtrl.js +++ b/src/muya/lib/contentState/arrowCtrl.js @@ -15,38 +15,42 @@ const adjustOffset = (offset, block, event) => { const arrowCtrl = ContentState => { ContentState.prototype.findNextRowCell = function (cell) { - if (!/th|td/.test(cell.type)) { + if (cell.functionType !== 'cellContent') { throw new Error(`block with type ${cell && cell.type} is not a table cell`) } - const row = this.getParent(cell) - const rowContainer = this.getParent(row) // thead or tbody - const column = row.children.indexOf(cell) + const thOrTd = this.getParent(cell) + const row = this.closest(cell, 'tr') + const rowContainer = this.closest(row, /thead|tbody/) // thead or tbody + const column = row.children.indexOf(thOrTd) if (rowContainer.type === 'thead') { const tbody = this.getNextSibling(rowContainer) if (tbody && tbody.children.length) { - return tbody.children[0].children[column] + return tbody.children[0].children[column].children[0] } } else if (rowContainer.type === 'tbody') { const nextRow = this.getNextSibling(row) if (nextRow) { - return nextRow.children[column] + return nextRow.children[column].children[0] } } return null } ContentState.prototype.findPrevRowCell = function (cell) { - if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`) - const row = this.getParent(cell) + if (cell.functionType !== 'cellContent') { + throw new Error(`block with type ${cell && cell.type} is not a table cell`) + } + const thOrTd = this.getParent(cell) + const row = this.closest(cell, 'tr') const rowContainer = this.getParent(row) // thead or tbody const rowIndex = rowContainer.children.indexOf(row) - const column = row.children.indexOf(cell) + const column = row.children.indexOf(thOrTd) if (rowContainer.type === 'tbody') { if (rowIndex === 0 && rowContainer.preSibling) { const thead = this.getPreSibling(rowContainer) - return thead.children[0].children[column] + return thead.children[0].children[column].children[0] } else if (rowIndex > 0) { - return this.getPreSibling(row).children[column] + return this.getPreSibling(row).children[column].children[0] } return null } @@ -118,12 +122,12 @@ const arrowCtrl = ContentState => { (event.key === EVENT_KEYS.ArrowUp && topOffset > 0) || (event.key === EVENT_KEYS.ArrowDown && bottomOffset > 0) ) { - if (!/pre|th|td/.test(block.type)) { + if (!/pre/.test(block.type) || block.functionType !== 'cellContent') { return } } - if (/th|td/.test(block.type)) { + if (block.functionType === 'cellContent') { let activeBlock const cellInNextRow = this.findNextRowCell(block) const cellInPrevRow = this.findPrevRowCell(block) diff --git a/src/muya/lib/contentState/backspaceCtrl.js b/src/muya/lib/contentState/backspaceCtrl.js index 0f898d99..83197a74 100644 --- a/src/muya/lib/contentState/backspaceCtrl.js +++ b/src/muya/lib/contentState/backspaceCtrl.js @@ -107,6 +107,10 @@ const backspaceCtrl = ContentState => { event.preventDefault() return this.deleteImage(this.selectedImage) } + if (this.selectedTableCells) { + event.preventDefault() + return this.deleteSelectedTableCells() + } } ContentState.prototype.backspaceHandler = function (event) { @@ -122,6 +126,7 @@ const backspaceCtrl = ContentState => { return this.deleteImage(this.selectedImage) } + // Handle select all content. if (this.isSelectAll()) { event.preventDefault() this.blocks = [this.createBlockP()] @@ -202,7 +207,8 @@ const backspaceCtrl = ContentState => { // fix bug when the first block is table, these two ways will cause bugs. // 1. one paragraph bollow table, selectAll, press backspace. // 2. select table from the first cell to the last cell, press backsapce. - if (/th/.test(startBlock.type) && start.offset === 0 && !startBlock.preSibling) { + const maybeCell = this.getParent(startBlock) + if (/th/.test(maybeCell.type) && start.offset === 0 && !maybeCell.preSibling) { if ( end.offset === endBlock.text.length && startOutmostBlock === endOutmostBlock && @@ -211,7 +217,7 @@ const backspaceCtrl = ContentState => { ) { event.preventDefault() // need remove the figure block. - const figureBlock = this.getBlock(this.findFigure(startBlock)) + const figureBlock = this.getBlock(this.closest(startBlock, 'figure')) // if table is the only block, need create a p block. const p = this.createBlockP(endBlock.text.substring(end.offset)) this.insertBefore(p, figureBlock) @@ -231,6 +237,21 @@ const backspaceCtrl = ContentState => { } } + // Fixed #1456 existed bugs `Select one cell and press backspace will cause bug` + if (startBlock.functionType === 'cellContent' && this.cursor.start.offset === 0 && this.cursor.end.offset !== 0 && this.cursor.end.offset === startBlock.text.length) { + event.preventDefault() + event.stopPropagation() + startBlock.text = '' + const { key } = startBlock + const offset = 0 + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + + return this.singleRender(startBlock) + } + // If select multiple paragraph or multiple characters in one paragraph, just let // inputCtrl to handle this case. if (start.key !== end.key || start.offset !== end.offset) { @@ -282,7 +303,7 @@ const backspaceCtrl = ContentState => { } // Fix issue #1218 - if (/th|td/.test(startBlock.type) && /.{1}$/.test(startBlock.text)) { + if (startBlock.functionType === 'cellContent' && /.{1}$/.test(startBlock.text)) { event.preventDefault() event.stopPropagation() @@ -298,11 +319,26 @@ const backspaceCtrl = ContentState => { return this.singleRender(startBlock) } + // Fix delete the last character in table cell, the default action will delete the cell content if not preventDefault. + if (startBlock.functionType === 'cellContent' && left === 1 && right === 0) { + event.stopPropagation() + event.preventDefault() + startBlock.text = '' + const { key } = startBlock + const offset = 0 + this.cursor = { + start: { key, offset }, + end: { key, offset } + } + + return this.singleRender(startBlock) + } + const tableHasContent = table => { const tHead = table.children[0] const tBody = table.children[1] - const tHeadHasContent = tHead.children[0].children.some(th => th.text.trim()) - const tBodyHasContent = tBody.children.some(row => row.children.some(td => td.text.trim())) + const tHeadHasContent = tHead.children[0].children.some(th => th.children[0].text.trim()) + const tBodyHasContent = tBody.children.some(row => row.children.some(td => td.children[0].text.trim())) return tHeadHasContent || tBodyHasContent } @@ -348,24 +384,23 @@ const backspaceCtrl = ContentState => { } this.partialRender() } - } else if (left === 0 && /th|td/.test(block.type)) { + } else if (left === 0 && block.functionType === 'cellContent') { event.preventDefault() event.stopPropagation() - const tHead = this.getBlock(parent.parent) - const table = this.getBlock(tHead.parent) - const figure = this.getBlock(table.parent) + const table = this.closest(block, 'table') + const figure = this.closest(table, 'figure') const hasContent = tableHasContent(table) let key let offset - if ((!preBlock || !/th|td/.test(preBlock.type)) && !hasContent) { - const newLine = this.createBlock('span') + if ((!preBlock || preBlock.functionType !== 'cellContent') && !hasContent) { + const paragraphContent = this.createBlock('span') delete figure.functionType figure.children = [] - this.appendChild(figure, newLine) + this.appendChild(figure, paragraphContent) figure.text = '' figure.type = 'p' - key = newLine.key + key = paragraphContent.key offset = 0 } else if (preBlock) { key = preBlock.key diff --git a/src/muya/lib/contentState/copyCutCtrl.js b/src/muya/lib/contentState/copyCutCtrl.js index f5a055e4..d3159e33 100644 --- a/src/muya/lib/contentState/copyCutCtrl.js +++ b/src/muya/lib/contentState/copyCutCtrl.js @@ -6,7 +6,18 @@ import ExportMarkdown from '../utils/exportMarkdown' import marked from '../parser/marked' const copyCutCtrl = ContentState => { + ContentState.prototype.docCutHandler = function (event) { + const { selectedTableCells } = this + if (selectedTableCells) { + event.preventDefault() + return this.deleteSelectedTableCells(true) + } + } + ContentState.prototype.cutHandler = function () { + if (this.selectedTableCells) { + return + } const { selectedImage } = this if (selectedImage) { const { key, token } = selectedImage @@ -179,10 +190,49 @@ const copyCutCtrl = ContentState => { let htmlData = wrapper.innerHTML const textData = this.htmlToMarkdown(htmlData) htmlData = marked(textData) + return { html: htmlData, text: textData } } - ContentState.prototype.copyHandler = function (event, type) { + ContentState.prototype.docCopyHandler = function (event) { + const { selectedTableCells } = this + if (selectedTableCells) { + event.preventDefault() + const { row, column, cells } = selectedTableCells + const figureBlock = this.createBlock('figure', { + functionType: 'table' + }) + const tableContents = [] + let i + let j + for (i = 0; i < row; i++) { + const rowWrapper = [] + for (j = 0; j < column; j++) { + const cell = cells[i * column + j] + + rowWrapper.push({ + text: cell.text, + align: cell.align + }) + } + tableContents.push(rowWrapper) + } + + const table = this.createTableInFigure({ rows: row, columns: column }, tableContents) + this.appendChild(figureBlock, table) + const listIndentation = this.listIndentation + const markdown = new ExportMarkdown([figureBlock], listIndentation).generate() + + event.clipboardData.setData('text/html', '') + event.clipboardData.setData('text/plain', markdown) + } + } + + ContentState.prototype.copyHandler = function (event, type, copyInfo = null) { + if (this.selectedTableCells) { + // Hand over to docCopyHandler + return + } event.preventDefault() const { selectedImage } = this if (selectedImage) { @@ -210,11 +260,13 @@ const copyCutCtrl = ContentState => { event.clipboardData.setData('text/plain', getSanitizeHtml(text)) break } - case 'copyTable': { - const table = this.getTableBlock() - if (!table) return + + case 'copyBlock': { + const block = typeof copyInfo === 'string' ? this.getBlock(copyInfo) : copyInfo + if (!block) return + const anchor = this.getAnchor(block) const listIndentation = this.listIndentation - const markdown = new ExportMarkdown([table], listIndentation).generate() + const markdown = new ExportMarkdown([anchor], listIndentation).generate() event.clipboardData.setData('text/html', '') event.clipboardData.setData('text/plain', markdown) break diff --git a/src/muya/lib/contentState/deleteCtrl.js b/src/muya/lib/contentState/deleteCtrl.js index 3ec7b50e..877af56b 100644 --- a/src/muya/lib/contentState/deleteCtrl.js +++ b/src/muya/lib/contentState/deleteCtrl.js @@ -1,6 +1,14 @@ import selection from '../selection' const deleteCtrl = ContentState => { + // Handle `delete` keydown event on document. + ContentState.prototype.docDeleteHandler = function (event) { + if (this.selectedTableCells) { + event.preventDefault() + return this.deleteSelectedTableCells() + } + } + ContentState.prototype.deleteHandler = function (event) { const { start, end } = selection.getCursorRange() if (!start || !end) { diff --git a/src/muya/lib/contentState/enterCtrl.js b/src/muya/lib/contentState/enterCtrl.js index 8aba29c5..1a0958f8 100644 --- a/src/muya/lib/contentState/enterCtrl.js +++ b/src/muya/lib/contentState/enterCtrl.js @@ -49,18 +49,23 @@ const enterCtrl = ContentState => { return container } - ContentState.prototype.createRow = function (row) { - const trBlock = this.createBlock('tr') + ContentState.prototype.createRow = function (row, isHeader = false) { + const tr = this.createBlock('tr') const len = row.children.length let i for (i = 0; i < len; i++) { - const tdBlock = this.createBlock('td') - const preChild = row.children[i] - tdBlock.column = i - tdBlock.align = preChild.align - this.appendChild(trBlock, tdBlock) + const cell = this.createBlock(isHeader ? 'th' : 'td', { + align: row.children[i].align, + column: i + }) + const cellContent = this.createBlock('span', { + functionType: 'cellContent' + }) + + this.appendChild(cell, cellContent) + this.appendChild(tr, cell) } - return trBlock + return tr } ContentState.prototype.createBlockLi = function (paragraphInListItem) { @@ -264,7 +269,7 @@ const enterCtrl = ContentState => { // Insert `
` in table cell if you want to open a new line. // Why not use `soft line break` or `hard line break` ? // Becasuse table cell only have one line. - if (event.shiftKey && /th|td/.test(block.type)) { + if (event.shiftKey && block.functionType === 'cellContent') { const { text, key } = block const brTag = '
' block.text = text.substring(0, start.offset) + brTag + text.substring(start.offset) @@ -297,19 +302,27 @@ const enterCtrl = ContentState => { } // handle enter in table - if (/th|td/.test(block.type)) { - const row = this.getBlock(block.parent) + if (block.functionType === 'cellContent') { + const row = this.closest(block, 'tr') const rowContainer = this.getBlock(row.parent) - const table = this.getBlock(rowContainer.parent) + const table = this.closest(rowContainer, 'table') if ( (isOsx && event.metaKey) || (!isOsx && event.ctrlKey) ) { - const nextRow = this.createRow(row) + const nextRow = this.createRow(row, false) if (rowContainer.type === 'thead') { - const tBody = this.getBlock(rowContainer.nextSibling) - this.insertBefore(nextRow, tBody.children[0]) + let tBody = this.getBlock(rowContainer.nextSibling) + if (!tBody) { + tBody = this.createBlock('tbody') + this.appendChild(table, tBody) + } + if (tBody.children.length) { + this.insertBefore(nextRow, tBody.children[0]) + } else { + this.appendChild(tBody, nextRow) + } } else { this.insertAfter(nextRow, row) } diff --git a/src/muya/lib/contentState/index.js b/src/muya/lib/contentState/index.js index 93e5de1a..f7d02ac2 100644 --- a/src/muya/lib/contentState/index.js +++ b/src/muya/lib/contentState/index.js @@ -8,7 +8,8 @@ import backspaceCtrl from './backspaceCtrl' import deleteCtrl from './deleteCtrl' import codeBlockCtrl from './codeBlockCtrl' import tableBlockCtrl from './tableBlockCtrl' -import selectionCtrl from './selectionCtrl' +import tableDragBarCtrl from './tableDragBarCtrl' +import tableSelectCellsCtrl from './tableSelectCellsCtrl' import History from './history' import arrowCtrl from './arrowCtrl' import pasteCtrl from './pasteCtrl' @@ -33,7 +34,6 @@ import escapeCharactersMap, { escapeCharacters } from '../parser/escapeCharacter const prototypes = [ tabCtrl, enterCtrl, - selectionCtrl, updateCtrl, backspaceCtrl, deleteCtrl, @@ -42,6 +42,8 @@ const prototypes = [ pasteCtrl, copyCutCtrl, tableBlockCtrl, + tableDragBarCtrl, + tableSelectCellsCtrl, paragraphCtrl, formatCtrl, searchCtrl, @@ -80,9 +82,37 @@ class ContentState { this.turndownConfig = Object.assign(DEFAULT_TURNDOWN_CONFIG, { bulletListMarker }) this.fontSize = 16 this.lineHeight = 1.6 + // table drag bar + this.dragInfo = null + this.isDragTableBar = false + this.dragEventIds = [] + // table cell select + this.cellSelectInfo = null + this._selectedTableCells = null + this.cellSelectEventIds = [] this.init() } + set selectedTableCells (info) { + const oldSelectedTableCells = this._selectedTableCells + if (!info && !!oldSelectedTableCells) { + const selectedCells = this.muya.container.querySelectorAll('.ag-cell-selected') + + for (const cell of Array.from(selectedCells)) { + cell.classList.remove('ag-cell-selected') + cell.classList.remove('ag-cell-border-top') + cell.classList.remove('ag-cell-border-right') + cell.classList.remove('ag-cell-border-bottom') + cell.classList.remove('ag-cell-border-left') + } + } + this._selectedTableCells = info + } + + get selectedTableCells () { + return this._selectedTableCells + } + set selectedImage (image) { const oldSelectedImage = this._selectedImage // if there is no selected image, remove selected status of current selected image. @@ -411,26 +441,16 @@ class ContentState { } } - // help func in removeBlocks - findFigure (block) { - if (block.type === 'figure') { - return block.key - } else { - const parent = this.getBlock(block.parent) - return this.findFigure(parent) - } - } - /** * remove blocks between before and after, and includes after block. */ removeBlocks (before, after, isRemoveAfter = true, isRecursion = false) { if (!isRecursion) { if (/td|th/.test(before.type)) { - this.exemption.add(this.findFigure(before)) + this.exemption.add(this.closest(before, 'figure')) } if (/td|th/.test(after.type)) { - this.exemption.add(this.findFigure(after)) + this.exemption.add(this.closest(after, 'figure')) } } let nextSibling = this.getBlock(before.nextSibling) @@ -712,6 +732,31 @@ class ContentState { return this.lastInDescendant(blocks[len - 1]) } + closest (block, type) { + if (!block) { + return null + } + if (type instanceof RegExp ? type.test(block.type) : block.type === type) { + return block + } else { + const parent = this.getParent(block) + return this.closest(parent, type) + } + } + + getAnchor (block) { + const { type, functionType } = block + if (type !== 'span') { + return null + } + + if (functionType === 'codeContent' || functionType === 'cellContent') { + return this.closest(block, 'figure') || this.closest(block, 'pre') + } else { + return this.getParent(block) + } + } + clear () { this.history.clearHistory() } diff --git a/src/muya/lib/contentState/paragraphCtrl.js b/src/muya/lib/contentState/paragraphCtrl.js index 4eddea0f..21dc7185 100644 --- a/src/muya/lib/contentState/paragraphCtrl.js +++ b/src/muya/lib/contentState/paragraphCtrl.js @@ -679,15 +679,24 @@ const paragraphCtrl = ContentState => { this.partialRender() return this.muya.eventCenter.dispatch('stateChange') } + // delete current paragraph - ContentState.prototype.deleteParagraph = function () { - const { start, end } = this.cursor - const startOutmostBlock = this.findOutMostBlock(this.getBlock(start.key)) - const endOutmostBlock = this.findOutMostBlock(this.getBlock(end.key)) - if (startOutmostBlock !== endOutmostBlock) { - // if the cursor is not in one paragraph, just return - return + ContentState.prototype.deleteParagraph = function (blockKey) { + let startOutmostBlock + if (blockKey) { + const block = this.getBlock(blockKey) + const firstEditableBlock = this.firstInDescendant(block) + startOutmostBlock = this.getAnchor(firstEditableBlock) + } else { + const { start, end } = this.cursor + startOutmostBlock = this.findOutMostBlock(this.getBlock(start.key)) + const endOutmostBlock = this.findOutMostBlock(this.getBlock(end.key)) + if (startOutmostBlock !== endOutmostBlock) { + // if the cursor is not in one paragraph, just return + return + } } + const preBlock = this.getBlock(startOutmostBlock.preSibling) const nextBlock = this.getBlock(startOutmostBlock.nextSibling) let cursorBlock = null @@ -723,25 +732,73 @@ const paragraphCtrl = ContentState => { !this.muya.keyboard.isComposed } - ContentState.prototype.selectAll = function () { - const { start } = this.cursor - const startBlock = this.getBlock(start.key) - // const endBlock = this.getBlock(end.key) - // handle selectAll in table. only select the startBlock cell... - if (/th|td/.test(startBlock.type)) { - const { key } = start - const textLength = startBlock.text.length - this.cursor = { - start: { - key, - offset: 0 - }, - end: { - key, - offset: textLength - } + ContentState.prototype.selectAllContent = function () { + const firstTextBlock = this.getFirstBlock() + const lastTextBlock = this.getLastBlock() + this.cursor = { + start: { + key: firstTextBlock.key, + offset: 0 + }, + end: { + key: lastTextBlock.key, + offset: lastTextBlock.text.length + } + } + + return this.render() + } + + ContentState.prototype.selectAll = function () { + const mayBeCell = this.isSingleCellSelected() + const mayBeTable = this.isWholeTableSelected() + if (mayBeTable) { + this.selectedTableCells = null + return this.selectAllContent() + } + // Select whole table if already select one cell. + if (mayBeCell) { + const table = this.closest(mayBeCell, 'table') + if (table) { + return this.selectTable(table) + } + } + const { start, end } = this.cursor + const startBlock = this.getBlock(start.key) + const endBlock = this.getBlock(end.key) + // handle selectAll in table. + if (startBlock.functionType === 'cellContent' && endBlock.functionType === 'cellContent') { + if (start.key === end.key) { + const table = this.closest(startBlock, 'table') + const cellBlock = this.closest(startBlock, /th|td/) + this.selectedTableCells = { + tableId: table.key, + row: 1, + column: 1, + cells: [{ + key: cellBlock.key, + top: true, + right: true, + bottom: true, + left: true + }] + } + this.muya.blur() + this.singleRender(table, false) + return this.muya.eventCenter.dispatch('muya-format-picker', { reference: null }) + } else { + const startTable = this.closest(startBlock, 'table') + const endTable = this.closest(endBlock, 'table') + // Check whether both blocks are in the same table. + if (!startTable || !endTable) { + console.error('No table found or invalid type.') + return + } else if (startTable.key !== endTable.key) { + // Select entire document + return + } + return this.selectTable(startTable) } - return this.partialRender() } // Handler selectAll in code block. only select all the code block conent. // `code block` here is Math, HTML, BLOCK CODE, Mermaid, vega-lite, flowchart, front-matter etc... @@ -774,19 +831,8 @@ const paragraphCtrl = ContentState => { } return this.partialRender() } - const firstTextBlock = this.getFirstBlock() - const lastTextBlock = this.getLastBlock() - this.cursor = { - start: { - key: firstTextBlock.key, - offset: 0 - }, - end: { - key: lastTextBlock.key, - offset: lastTextBlock.text.length - } - } - this.render() + + return this.selectAllContent() } } diff --git a/src/muya/lib/contentState/pasteCtrl.js b/src/muya/lib/contentState/pasteCtrl.js index d378b10e..79fd80f3 100644 --- a/src/muya/lib/contentState/pasteCtrl.js +++ b/src/muya/lib/contentState/pasteCtrl.js @@ -329,7 +329,7 @@ const pasteCtrl = ContentState => { return this.partialRender() } - if (/th|td/.test(startBlock.type)) { + if (startBlock.functionType === 'cellContent') { const pendingText = text.trim().replace(/\n/g, '
') startBlock.text = startBlock.text.substring(0, start.offset) + pendingText + startBlock.text.substring(end.offset) const { key } = startBlock diff --git a/src/muya/lib/contentState/selectionCtrl.js b/src/muya/lib/contentState/selectionCtrl.js deleted file mode 100644 index c3392a8a..00000000 --- a/src/muya/lib/contentState/selectionCtrl.js +++ /dev/null @@ -1,58 +0,0 @@ -const selectionCtrl = ContentState => { - // Returns the table from the table cell: - // table <-- thead or tbody <-- tr <-- th or td (cell) - ContentState.prototype.getTableFromTableCell = function (block) { - const table = this.getParent(this.getParent(this.getParent(block))) - if (table && table.type !== 'table') { - return null - } - return table - } - - ContentState.prototype.tableCellHandler = function (event) { - const { start, end } = this.cursor - const startBlock = this.getBlock(start.key) - const endBlock = this.getBlock(end.key) - const { type: startType } = startBlock - const { type: endType } = endBlock - if (/th|td/.test(startType) && /th|td/.test(endType)) { - if (start.key === end.key) { - const { text, key } = startBlock - this.cursor = { - start: { key, offset: 0 }, - end: { key, offset: text.length } - } - } else { - const startTable = this.getTableFromTableCell(startBlock) - const endTable = this.getTableFromTableCell(endBlock) - // Check whether both blocks are in the same table. - if (!startTable || !endTable) { - console.error('No table found or invalid type.') - return - } else if (startTable.key !== endTable.key) { - // Select entire document - return - } - const firstTableCell = this.firstInDescendant(startTable) - const lastTableCell = this.lastInDescendant(startTable) - if (!firstTableCell || !/th|td/.test(firstTableCell.type) || - !lastTableCell || !/th|td/.test(lastTableCell.type)) { - console.error('No table cell found or invalid type.') - return - } - const { key: startKey } = firstTableCell - const { key: endKey, text } = lastTableCell - this.cursor = { - start: { key: startKey, offset: 0 }, - end: { key: endKey, offset: text.length } - } - } - - event.preventDefault() - event.stopPropagation() - this.partialRender() - } - } -} - -export default selectionCtrl diff --git a/src/muya/lib/contentState/tabCtrl.js b/src/muya/lib/contentState/tabCtrl.js index 4565e145..5d09f97a 100644 --- a/src/muya/lib/contentState/tabCtrl.js +++ b/src/muya/lib/contentState/tabCtrl.js @@ -40,25 +40,27 @@ const BOTH_SIDES_FORMATS = ['strong', 'em', 'inline_code', 'image', 'link', 'ref const tabCtrl = ContentState => { ContentState.prototype.findNextCell = function (block) { - if (!(/td|th/.test(block.type))) { + if (block.functionType !== 'cellContent') { throw new Error('only th and td can have next cell') } - const nextSibling = this.getBlock(block.nextSibling) - const parent = this.getBlock(block.parent) - const tbOrTh = this.getBlock(parent.parent) + const cellBlock = this.getParent(block) + const nextSibling = this.getBlock(cellBlock.nextSibling) + const rowBlock = this.getBlock(cellBlock.parent) + const tbOrTh = this.getBlock(rowBlock.parent) if (nextSibling) { - return nextSibling + return this.firstInDescendant(nextSibling) } else { - if (parent.nextSibling) { - const nextRow = this.getBlock(parent.nextSibling) - return nextRow.children[0] + if (rowBlock.nextSibling) { + const nextRow = this.getBlock(rowBlock.nextSibling) + return this.firstInDescendant(nextRow) } else if (tbOrTh.type === 'thead') { const tBody = this.getBlock(tbOrTh.nextSibling) if (tBody && tBody.children.length) { - return tBody.children[0].children[0] + return this.firstInDescendant(tBody) } } } + return false } @@ -369,19 +371,22 @@ const tabCtrl = ContentState => { // Handle `tab` key in table cell. let nextCell - if (start.key === end.key && /th|td/.test(startBlock.type)) { + if (start.key === end.key && startBlock.functionType === 'cellContent') { nextCell = this.findNextCell(startBlock) - } else if (/th|td/.test(endBlock.type)) { + } else if (endBlock.functionType === 'cellContent') { nextCell = endBlock } if (nextCell) { - const key = nextCell.key + const { key } = nextCell + + const offset = 0 this.cursor = { - start: { key, offset: 0 }, - end: { key, offset: 0 } + start: { key, offset }, + end: { key, offset } } - return this.partialRender() + const figure = this.closest(nextCell, 'figure') + return this.singleRender(figure) } if (this.isIndentableListItem()) { diff --git a/src/muya/lib/contentState/tableBlockCtrl.js b/src/muya/lib/contentState/tableBlockCtrl.js index ddfd0c43..38ae68eb 100644 --- a/src/muya/lib/contentState/tableBlockCtrl.js +++ b/src/muya/lib/contentState/tableBlockCtrl.js @@ -3,25 +3,32 @@ import { isLengthEven, getParagraphReference } from '../utils' const TABLE_BLOCK_REG = /^\|.*?(\\*)\|.*?(\\*)\|/ const tableBlockCtrl = ContentState => { - ContentState.prototype.createTableInFigure = function ({ rows, columns }, headerTexts) { - const table = this.createBlock('table') + ContentState.prototype.createTableInFigure = function ({ rows, columns }, tableContents = []) { + const table = this.createBlock('table', { + row: rows - 1, // zero base + column: columns - 1 + }) const tHead = this.createBlock('thead') const tBody = this.createBlock('tbody') - table.row = rows - 1 // zero base - table.column = columns - 1 // zero base let i let j for (i = 0; i < rows; i++) { const rowBlock = this.createBlock('tr') i === 0 ? this.appendChild(tHead, rowBlock) : this.appendChild(tBody, rowBlock) + const rowContents = tableContents[i] for (j = 0; j < columns; j++) { const cell = this.createBlock(i === 0 ? 'th' : 'td', { - text: headerTexts && i === 0 ? headerTexts[j] : '' + align: rowContents ? rowContents[j].align : '', + column: j }) + const cellContent = this.createBlock('span', { + text: rowContents ? rowContents[j].text : '', + functionType: 'cellContent' + }) + + this.appendChild(cell, cellContent) this.appendChild(rowBlock, cell) - cell.align = '' - cell.column = j } } @@ -33,52 +40,25 @@ const tableBlockCtrl = ContentState => { return table } - ContentState.prototype.closest = function (block, type) { - if (!block) { - return null - } - if (block.type === type) { - return block - } else { - const parent = this.getParent(block) - return this.closest(parent, type) - } - } - - ContentState.prototype.getAnchor = function (block) { - const { type, functionType } = block - switch (type) { - case 'span': - if (functionType === 'codeContent') { - return this.closest(block, 'figure') || this.closest(block, 'pre') - } else { - return this.getParent(block) - } - - case 'th': - case 'td': - return this.closest(block, 'figure') - - default: - return null - } - } - ContentState.prototype.createFigure = function ({ rows, columns }) { const { end } = this.cursor const table = this.createTableInFigure({ rows, columns }) - const figureBlock = this.createBlock('figure') - figureBlock.functionType = 'table' + const figureBlock = this.createBlock('figure', { + functionType: 'table' + }) const endBlock = this.getBlock(end.key) const anchor = this.getAnchor(endBlock) - if (!anchor) return + if (!anchor) { + return + } + this.insertAfter(figureBlock, anchor) if (/p|h\d/.test(anchor.type) && !endBlock.text) { this.removeBlock(anchor) } this.appendChild(figureBlock, table) - const key = table.children[0].children[0].children[0].key // fist cell key in thead + const { key } = this.firstInDescendant(table) // fist cell key in thead const offset = 0 this.cursor = { start: { key, offset }, @@ -112,10 +92,11 @@ const tableBlockCtrl = ContentState => { rowHeader.push('') } } + const columns = rowHeader.length const rows = 2 - const table = this.createTableInFigure({ rows, columns }, rowHeader) + const table = this.createTableInFigure({ rows, columns }, [rowHeader.map(text => ({ text, align: '' }))]) block.type = 'figure' block.text = '' @@ -123,21 +104,20 @@ const tableBlockCtrl = ContentState => { block.functionType = 'table' this.appendChild(block, table) - return table.children[1].children[0].children[0] // first cell in tbody + return this.firstInDescendant(table.children[1]) // first cell content in tbody } ContentState.prototype.tableToolBarClick = function (type) { const { start: { key } } = this.cursor const block = this.getBlock(key) - if (!(/td|th/.test(block.type))) throw new Error('table is not active') - const { column, align } = block - const getTable = td => { - const row = this.getBlock(block.parent) - const rowContainer = this.getBlock(row.parent) - return this.getBlock(rowContainer.parent) + const parentBlock = this.getParent(block) + if (block.functionType !== 'cellContent') { + throw new Error('table is not active') } - const table = getTable(block) + const { column, align } = parentBlock + const table = this.closest(block, 'table') const figure = this.getBlock(table.parent) + switch (type) { case 'left': case 'center': @@ -171,26 +151,37 @@ const tableBlockCtrl = ContentState => { case 'table': { const { eventCenter } = this.muya const figureKey = figure.key - const tableLable = document.querySelector(`#${figureKey} [data-label=table]`) + const tableEle = document.querySelector(`#${figureKey} [data-label=table]`) const { row = 1, column = 1 } = table // zero base const handler = (row, column) => { const { row: oldRow, column: oldColumn } = table - const tBody = table.children[1] + let tBody = table.children[1] const tHead = table.children[0] const headerRow = tHead.children[0] - const bodyRows = tBody.children + const bodyRows = tBody ? tBody.children : [] let i if (column > oldColumn) { for (i = oldColumn + 1; i <= column; i++) { - const th = this.createBlock('th') - th.column = i - th.align = '' + const th = this.createBlock('th', { + column: i, + align: '' + }) + const thContent = this.createBlock('span', { + functionType: 'cellContent' + }) + this.appendChild(th, thContent) this.appendChild(headerRow, th) bodyRows.forEach(bodyRow => { - const td = this.createBlock('td') - td.column = i - td.align = '' + const td = this.createBlock('td', { + column: i, + align: '' + }) + + const tdContent = this.createBlock('span', { + functionType: 'cellContent' + }) + this.appendChild(td, tdContent) this.appendChild(bodyRow, td) }) } @@ -209,16 +200,25 @@ const tableBlockCtrl = ContentState => { const lastRow = tBody.children[tBody.children.length - 1] this.removeBlock(lastRow) } + if (tBody.children.length === 0) { + this.removeBlock(tBody) + } } else if (row > oldRow) { - const oneRowInBody = bodyRows[0] + if (!tBody) { + tBody = this.createBlock('tbody') + this.appendChild(table, tBody) + } + const oneHeaderRow = tHead.children[0] for (i = oldRow + 1; i <= row; i++) { - const bodyRow = this.createRow(oneRowInBody) + const bodyRow = this.createRow(oneHeaderRow, false) + this.appendChild(tBody, bodyRow) } } + Object.assign(table, { row, column }) - const cursorBlock = headerRow.children[0] + const cursorBlock = this.firstInDescendant(headerRow) const key = cursorBlock.key const offset = cursorBlock.text.length this.cursor = { @@ -228,66 +228,69 @@ const tableBlockCtrl = ContentState => { this.muya.eventCenter.dispatch('stateChange') this.partialRender() } - const reference = getParagraphReference(tableLable, tableLable.id) + + const reference = getParagraphReference(tableEle, tableEle.id) eventCenter.dispatch('muya-table-picker', { row, column }, reference, handler.bind(this)) } } } // insert/remove row/column - ContentState.prototype.editTable = function ({ location, action, target }) { - const { start, end } = this.cursor - const block = this.getBlock(start.key) - if (start.key !== end.key || !/th|td/.test(block.type)) { + ContentState.prototype.editTable = function ({ location, action, target }, cellContentKey) { + let block + let start + let end + if (cellContentKey) { + block = this.getBlock(cellContentKey) + } else { + ({ start, end } = this.cursor) + if (start.key !== end.key) { + throw new Error('Cursor is not in one block, can not editTable') + } + + block = this.getBlock(start.key) + } + + if (block.functionType !== 'cellContent') { throw new Error('Cursor is not in table block, so you can not insert/edit row/column') } - const currentRow = this.getParent(block) - const rowContainer = this.getParent(currentRow) // tbody or thead - const table = this.getParent(rowContainer) + + const cellBlock = this.getParent(block) + const currentRow = this.getParent(cellBlock) + const table = this.closest(block, 'table') const thead = table.children[0] const tbody = table.children[1] - const { column } = table - const columnIndex = currentRow.children.indexOf(block) - let cursorBlock + const columnIndex = currentRow.children.indexOf(cellBlock) + // const rowIndex = rowContainer.type === 'thead' ? 0 : tbody.children.indexOf(currentRow) + 1 - const createRow = (column, isHeader) => { - const tr = this.createBlock('tr') - let i - for (i = 0; i <= column; i++) { - const cell = this.createBlock(isHeader ? 'th' : 'td') - cell.align = currentRow.children[i].align - cell.column = i - this.appendChild(tr, cell) - } - return tr - } + let cursorBlock if (target === 'row') { if (action === 'insert') { - const newRow = (location === 'previous' && block.type === 'th') - ? createRow(column, true) - : createRow(column, false) + const newRow = (location === 'previous' && cellBlock.type === 'th') + ? this.createRow(currentRow, true) + : this.createRow(currentRow, false) if (location === 'previous') { this.insertBefore(newRow, currentRow) - if (block.type === 'th') { + if (cellBlock.type === 'th') { this.removeBlock(currentRow) currentRow.children.forEach(cell => (cell.type = 'td')) const firstRow = tbody.children[0] this.insertBefore(currentRow, firstRow) } } else { - if (block.type === 'th') { + if (cellBlock.type === 'th') { const firstRow = tbody.children[0] this.insertBefore(newRow, firstRow) } else { this.insertAfter(newRow, currentRow) } } - cursorBlock = newRow.children[columnIndex] + cursorBlock = newRow.children[columnIndex].children[0] // handle remove row } else { if (location === 'previous') { - if (block.type === 'th') return + if (cellBlock.type === 'th') return if (!currentRow.preSibling) { const headRow = thead.children[0] if (!currentRow.nextSibling) return @@ -300,20 +303,20 @@ const tableBlockCtrl = ContentState => { this.removeBlock(preRow) } } else if (location === 'current') { - if (block.type === 'th' && tbody.children.length >= 2) { + if (cellBlock.type === 'th' && tbody.children.length >= 2) { const firstRow = tbody.children[0] this.removeBlock(currentRow) this.removeBlock(firstRow) this.appendChild(thead, firstRow) firstRow.children.forEach(cell => (cell.type = 'th')) - cursorBlock = firstRow.children[columnIndex] + cursorBlock = firstRow.children[columnIndex].children[0] } - if (block.type === 'td' && (currentRow.preSibling || currentRow.nextSibling)) { - cursorBlock = (this.getNextSibling(currentRow) || this.getPreSibling(currentRow)).children[columnIndex] + if (cellBlock.type === 'td' && (currentRow.preSibling || currentRow.nextSibling)) { + cursorBlock = (this.getNextSibling(currentRow) || this.getPreSibling(currentRow)).children[columnIndex].children[0] this.removeBlock(currentRow) } } else { - if (block.type === 'th') { + if (cellBlock.type === 'th') { if (tbody.children.length >= 2) { const firstRow = tbody.children[0] this.removeBlock(firstRow) @@ -322,7 +325,9 @@ const tableBlockCtrl = ContentState => { } } else { const nextRow = this.getNextSibling(currentRow) - if (nextRow) this.removeBlock(nextRow) + if (nextRow) { + this.removeBlock(nextRow) + } } } } @@ -330,8 +335,13 @@ const tableBlockCtrl = ContentState => { if (action === 'insert') { [...thead.children, ...tbody.children].forEach(tableRow => { const targetCell = tableRow.children[columnIndex] - const cell = this.createBlock(targetCell.type) - cell.align = '' + const cell = this.createBlock(targetCell.type, { + align: '' + }) + const cellContent = this.createBlock('span', { + functionType: 'cellContent' + }) + this.appendChild(cell, cellContent) if (location === 'left') { this.insertBefore(cell, targetCell) } else { @@ -341,7 +351,7 @@ const tableBlockCtrl = ContentState => { cell.column = i }) }) - cursorBlock = location === 'left' ? this.getPreSibling(block) : this.getNextSibling(block) + cursorBlock = location === 'left' ? this.getPreSibling(cellBlock).children[0] : this.getNextSibling(cellBlock).children[0] // handle remove column } else { if (currentRow.children.length <= 2) return @@ -350,7 +360,7 @@ const tableBlockCtrl = ContentState => { const removeCell = location === 'left' ? this.getPreSibling(targetCell) : (location === 'current' ? targetCell : this.getNextSibling(targetCell)) - if (removeCell === block) { + if (removeCell === cellBlock) { cursorBlock = this.findNextBlockInLocation(block) } @@ -388,8 +398,8 @@ const tableBlockCtrl = ContentState => { .filter(p => endParents.includes(p)) if (affiliation.length) { - const table = affiliation.find(p => p.type === 'figure') - return table + const figure = affiliation.find(p => p.type === 'figure') + return figure } } diff --git a/src/muya/lib/contentState/tableDragBarCtrl.js b/src/muya/lib/contentState/tableDragBarCtrl.js new file mode 100644 index 00000000..e5f53825 --- /dev/null +++ b/src/muya/lib/contentState/tableDragBarCtrl.js @@ -0,0 +1,364 @@ +const calculateAspects = (tableId, barType) => { + const table = document.querySelector(`#${tableId}`) + if (barType === 'bottom') { + const firstRow = table.querySelector('tr') + return Array.from(firstRow.children).map(cell => cell.clientWidth) + } else { + return Array.from(table.querySelectorAll('tr')).map(row => row.clientHeight) + } +} + +export const getAllTableCells = tableId => { + const table = document.querySelector(`#${tableId}`) + const rows = table.querySelectorAll('tr') + const cells = [] + for (const row of Array.from(rows)) { + cells.push(Array.from(row.children)) + } + + return cells +} + +export const getIndex = (barType, cell) => { + if (cell.tagName === 'SPAN') { + cell = cell.parentNode + } + const row = cell.parentNode + if (barType === 'bottom') { + return Array.from(row.children).indexOf(cell) + } else { + const rowContainer = row.parentNode + if (rowContainer.tagName === 'THEAD') { + return 0 + } else { + return Array.from(rowContainer.children).indexOf(row) + 1 + } + } +} + +const getDragCells = (tableId, barType, index) => { + const table = document.querySelector(`#${tableId}`) + const dragCells = [] + if (barType === 'left') { + if (index === 0) { + dragCells.push(...table.querySelectorAll('th')) + } else { + const row = table.querySelector('tbody').children[index - 1] + dragCells.push(...row.children) + } + } else { + const rows = Array.from(table.querySelectorAll('tr')) + const len = rows.length + let i + for (i = 0; i < len; i++) { + dragCells.push(rows[i].children[index]) + } + } + return dragCells +} + +const tableDragBarCtrl = ContentState => { + ContentState.prototype.handleMouseDown = function (event) { + event.preventDefault() + const { eventCenter } = this.muya + const { clientX, clientY, target } = event + const tableId = target.closest('table').id + const barType = target.classList.contains('left') ? 'left' : 'bottom' + const index = getIndex(barType, target) + const aspects = calculateAspects(tableId, barType) + this.dragInfo = { + tableId, + clientX, + clientY, + barType, + index, + curIndex: index, + dragCells: getDragCells(tableId, barType, index), + cells: getAllTableCells(tableId), + aspects, + offset: 0 + } + + for (const row of this.dragInfo.cells) { + for (const cell of row) { + if (!this.dragInfo.dragCells.includes(cell)) { + cell.classList.add('ag-cell-transform') + } + } + } + + const mouseMoveId = eventCenter.attachDOMEvent(document, 'mousemove', this.handleMouseMove.bind(this)) + const mouseUpId = eventCenter.attachDOMEvent(document, 'mouseup', this.handleMouseUp.bind(this)) + this.dragEventIds.push(mouseMoveId, mouseUpId) + } + + ContentState.prototype.handleMouseMove = function (event) { + if (!this.dragInfo) { + return + } + const { barType } = this.dragInfo + const attrName = barType === 'bottom' ? 'clientX' : 'clientY' + const offset = this.dragInfo.offset = event[attrName] - this.dragInfo[attrName] + if (Math.abs(offset) < 5) { + return + } + this.isDragTableBar = true + this.hideUnnecessaryBar() + this.calculateCurIndex() + this.setDragTargetStyle() + this.setSwitchStyle() + } + + ContentState.prototype.handleMouseUp = function (event) { + const { eventCenter } = this.muya + for (const id of this.dragEventIds) { + eventCenter.detachDOMEvent(id) + } + this.dragEventIds = [] + if (!this.isDragTableBar) { + return + } + + this.setDropTargetStyle() + + // The drop animation need 300ms. + setTimeout(() => { + this.switchTableData() + this.resetDragTableBar() + }, 300) + } + + ContentState.prototype.hideUnnecessaryBar = function () { + const { barType } = this.dragInfo + const hideClassName = barType === 'bottom' ? 'left' : 'bottom' + const needHideBar = document.querySelector(`.ag-drag-handler.${hideClassName}`) + if (needHideBar) { + needHideBar.style.display = 'none' + } + } + + ContentState.prototype.calculateCurIndex = function () { + let { offset, aspects, index } = this.dragInfo + let curIndex = index + const len = aspects.length + let i + if (offset > 0) { + for (i = index; i < len; i++) { + const aspect = aspects[i] + if (i === index) { + offset -= Math.floor(aspect / 2) + } else { + offset -= aspect + } + if (offset < 0) { + break + } else { + curIndex++ + } + } + } else if (offset < 0) { + for (i = index; i >= 0; i--) { + const aspect = aspects[i] + if (i === index) { + offset += Math.floor(aspect / 2) + } else { + offset += aspect + } + if (offset > 0) { + break + } else { + curIndex-- + } + } + } + + this.dragInfo.curIndex = Math.max(0, Math.min(curIndex, len - 1)) + } + + ContentState.prototype.setDragTargetStyle = function () { + const { offset, barType, dragCells } = this.dragInfo + + for (const cell of dragCells) { + if (!cell.classList.contains('ag-drag-cell')) { + cell.classList.add('ag-drag-cell') + cell.classList.add(`ag-drag-${barType}`) + } + const valueName = barType === 'bottom' ? 'translateX' : 'translateY' + cell.style.transform = `${valueName}(${offset}px)` + } + } + + ContentState.prototype.setSwitchStyle = function () { + const { index, offset, curIndex, barType, aspects, cells } = this.dragInfo + const aspect = aspects[index] + const len = aspects.length + + let i + if (offset > 0) { + if (barType === 'bottom') { + for (const row of cells) { + for (i = 0; i < len; i++) { + const cell = row[i] + if (i > index && i <= curIndex) { + cell.style.transform = `translateX(${-aspect}px)` + } else if (i !== index) { + cell.style.transform = 'translateX(0px)' + } + } + } + } else { + for (i = 0; i < len; i++) { + const row = cells[i] + for (const cell of row) { + if (i > index && i <= curIndex) { + cell.style.transform = `translateY(${-aspect}px)` + } else if (i !== index) { + cell.style.transform = 'translateY(0px)' + } + } + } + } + } else { + if (barType === 'bottom') { + for (const row of cells) { + for (i = 0; i < len; i++) { + const cell = row[i] + if (i >= curIndex && i < index) { + cell.style.transform = `translateX(${aspect}px)` + } else if (i !== index) { + cell.style.transform = 'translateX(0px)' + } + } + } + } else { + for (i = 0; i < len; i++) { + const row = cells[i] + for (const cell of row) { + if (i >= curIndex && i < index) { + cell.style.transform = `translateY(${aspect}px)` + } else if (i !== index) { + cell.style.transform = 'translateY(0px)' + } + } + } + } + } + } + + ContentState.prototype.setDropTargetStyle = function () { + const { dragCells, barType, curIndex, index, aspects, offset } = this.dragInfo + let move = 0 + let i + if (offset > 0) { + for (i = index + 1; i <= curIndex; i++) { + move += aspects[i] + } + } else { + for (i = curIndex; i < index; i++) { + move -= aspects[i] + } + } + for (const cell of dragCells) { + cell.classList.remove('ag-drag-cell') + cell.classList.remove(`ag-drag-${barType}`) + cell.classList.add('ag-cell-transform') + const valueName = barType === 'bottom' ? 'translateX' : 'translateY' + cell.style.transform = `${valueName}(${move}px)` + } + } + + ContentState.prototype.switchTableData = function () { + const { barType, index, curIndex, tableId, offset } = this.dragInfo + const table = this.getBlock(tableId) + const tHead = table.children[0] + const tBody = table.children[1] + const rows = [tHead.children[0], ...(tBody ? tBody.children : [])] + let i + + if (index !== curIndex) { + // Cursor in the same cell. + const { start, end } = this.cursor + let key = null + if (barType === 'bottom') { + for (const row of rows) { + const isCursorCell = row.children[index].children[0].key === start.key + const { text } = row.children[index].children[0] + const { align } = row.children[index] + if (offset > 0) { + for (i = index; i < curIndex; i++) { + row.children[i].children[0].text = row.children[i + 1].children[0].text + row.children[i].align = row.children[i + 1].align + } + row.children[curIndex].children[0].text = text + row.children[curIndex].align = align + } else { + for (i = index; i > curIndex; i--) { + row.children[i].children[0].text = row.children[i - 1].children[0].text + row.children[i].align = row.children[i - 1].align + } + row.children[curIndex].children[0].text = text + row.children[curIndex].align = align + } + if (isCursorCell) { + key = row.children[curIndex].children[0].key + } + } + } else { + let column = null + const temp = rows[index].children.map((cell, i) => { + if (cell.children[0].key === start.key) { + column = i + } + return cell.children[0].text + }) + if (offset > 0) { + for (i = index; i < curIndex; i++) { + rows[i].children.forEach((cell, ii) => { + cell.children[0].text = rows[i + 1].children[ii].children[0].text + }) + } + rows[curIndex].children.forEach((cell, i) => { + if (i === column) { + key = cell.children[0].key + } + cell.children[0].text = temp[i] + }) + } else { + for (i = index; i > curIndex; i--) { + rows[i].children.forEach((cell, ii) => { + cell.children[0].text = rows[i - 1].children[ii].children[0].text + }) + } + rows[curIndex].children.forEach((cell, i) => { + if (i === column) { + key = cell.children[0].key + } + cell.children[0].text = temp[i] + }) + } + } + if (key) { + this.cursor = { + start: { + key, + offset: start.offset + }, + end: { + key, + offset: end.offset + } + } + return this.singleRender(table) + } else { + return this.partialRender() + } + } + } + + ContentState.prototype.resetDragTableBar = function () { + this.dragInfo = null + this.isDragTableBar = false + } +} + +export default tableDragBarCtrl diff --git a/src/muya/lib/contentState/tableSelectCellsCtrl.js b/src/muya/lib/contentState/tableSelectCellsCtrl.js new file mode 100644 index 00000000..fcb89aa9 --- /dev/null +++ b/src/muya/lib/contentState/tableSelectCellsCtrl.js @@ -0,0 +1,266 @@ +import { getAllTableCells, getIndex } from './tableDragBarCtrl' + +const tableSelectCellsCtrl = ContentState => { + ContentState.prototype.handleCellMouseDown = function (event) { + if (event.buttons === 2) { + // the contextmenu is emit. + return + } + const { eventCenter } = this.muya + const { target } = event + const cell = target.closest('th') || target.closest('td') + const tableId = target.closest('table').id + const row = getIndex('left', cell) + const column = getIndex('bottom', cell) + this.cellSelectInfo = { + tableId, + anchor: { + key: cell.id, + row, + column + }, + focus: null, + isStartSelect: false, + cells: getAllTableCells(tableId), + selectedCells: [] + } + + const mouseMoveId = eventCenter.attachDOMEvent(document.body, 'mousemove', this.handleCellMouseMove.bind(this)) + const mouseUpId = eventCenter.attachDOMEvent(document.body, 'mouseup', this.handleCellMouseUp.bind(this)) + this.cellSelectEventIds.push(mouseMoveId, mouseUpId) + } + + ContentState.prototype.handleCellMouseMove = function (event) { + const { target } = event + const cell = target.closest('th') || target.closest('td') + const table = target.closest('table') + const isOverSameTableCell = cell && table && table.id === this.cellSelectInfo.tableId + if (isOverSameTableCell && cell.id !== this.cellSelectInfo.anchor.key) { + this.cellSelectInfo.isStartSelect = true + this.muya.blur() + } + if (isOverSameTableCell && this.cellSelectInfo.isStartSelect) { + const row = getIndex('left', cell) + const column = getIndex('bottom', cell) + this.cellSelectInfo.focus = { + key: cell.key, + row, + column + } + } else { + this.cellSelectInfo.focus = null + } + + this.calculateSelectedCells() + this.setSelectedCellsStyle() + } + + ContentState.prototype.handleCellMouseUp = function (event) { + const { eventCenter } = this.muya + for (const id of this.cellSelectEventIds) { + eventCenter.detachDOMEvent(id) + } + this.cellSelectEventIds = [] + if (this.cellSelectInfo && this.cellSelectInfo.isStartSelect) { + event.preventDefault() + const { tableId, selectedCells, anchor, focus } = this.cellSelectInfo + // Mouse up outside table, the focus is null + if (!focus) { + return + } + // We need to handle this after click event, because click event is emited after mouseup(mouseup will be followed by a click envent), but we set + // the `selectedTableCells` to null when click event emited. + setTimeout(() => { + this.selectedTableCells = { + tableId, + row: Math.abs(anchor.row - focus.row) + 1, // 1 base + column: Math.abs(anchor.column - focus.column) + 1, // 1 base + cells: selectedCells.map(c => { + delete c.ele + return c + }) + } + this.cellSelectInfo = null + const table = this.getBlock(tableId) + return this.singleRender(table, false) + }) + } + } + + ContentState.prototype.calculateSelectedCells = function () { + const { anchor, focus, cells } = this.cellSelectInfo + this.cellSelectInfo.selectedCells = [] + if (focus) { + const startRowIndex = Math.min(anchor.row, focus.row) + const endRowIndex = Math.max(anchor.row, focus.row) + const startColIndex = Math.min(anchor.column, focus.column) + const endColIndex = Math.max(anchor.column, focus.column) + let i + let j + for (i = startRowIndex; i <= endRowIndex; i++) { + const row = cells[i] + for (j = startColIndex; j <= endColIndex; j++) { + const cell = row[j] + const cellBlock = this.getBlock(cell.id) + this.cellSelectInfo.selectedCells.push({ + ele: cell, + key: cell.id, + text: cellBlock.children[0].text, + align: cellBlock.align, + top: i === startRowIndex, + right: j === endColIndex, + bottom: i === endRowIndex, + left: j === startColIndex + }) + } + } + } + } + + ContentState.prototype.setSelectedCellsStyle = function () { + const { selectedCells, cells } = this.cellSelectInfo + for (const row of cells) { + for (const cell of row) { + cell.classList.remove('ag-cell-selected') + cell.classList.remove('ag-cell-border-top') + cell.classList.remove('ag-cell-border-right') + cell.classList.remove('ag-cell-border-bottom') + cell.classList.remove('ag-cell-border-left') + } + } + + for (const cell of selectedCells) { + const { ele, top, right, bottom, left } = cell + ele.classList.add('ag-cell-selected') + if (top) { + ele.classList.add('ag-cell-border-top') + } + if (right) { + ele.classList.add('ag-cell-border-right') + } + if (bottom) { + ele.classList.add('ag-cell-border-bottom') + } + if (left) { + ele.classList.add('ag-cell-border-left') + } + } + } + + // Remove the content of selected table cell, delete the row/column if selected one row/column without content. + // Delete the table if the selected whole table is empty. + ContentState.prototype.deleteSelectedTableCells = function (isCut = false) { + const { tableId, cells } = this.selectedTableCells + const tableBlock = this.getBlock(tableId) + const { row, column } = tableBlock + const rows = new Set() + let lastColumn = null + let isSameColumn = true + let hasContent = false + for (const cell of cells) { + const cellBlock = this.getBlock(cell.key) + const rowBlock = this.getParent(cellBlock) + const { column: cellColumn } = cellBlock + rows.add(rowBlock) + if (cellBlock.children[0].text) { + hasContent = true + } + if (typeof lastColumn === 'object') { + lastColumn = cellColumn + } else if (cellColumn !== lastColumn) { + isSameColumn = false + } + cellBlock.children[0].text = '' + } + + const isOneColumnSelected = rows.size === +row + 1 && isSameColumn + const isOneRowSelected = cells.length === +column + 1 && rows.size === 1 + const isWholeTableSelected = rows.size === +row + 1 && cells.length === (+row + 1) * (+column + 1) + + if (isCut && isWholeTableSelected) { + return this.deleteParagraph(tableId) + } + + if (hasContent) { + this.singleRender(tableBlock, false) + + return this.muya.dispatchChange() + } else { + const cellKey = cells[0].key + const cellBlock = this.getBlock(cellKey) + const cellContentKey = cellBlock.children[0].key + if (isOneColumnSelected) { + // Remove one empty column + return this.editTable({ + location: 'current', + action: 'remove', + target: 'column' + }, cellContentKey) + } else if (isOneRowSelected) { + // Remove one empty row + return this.editTable({ + location: 'current', + action: 'remove', + target: 'row' + }, cellContentKey) + } else if (isWholeTableSelected) { + // Select whole empty table + return this.deleteParagraph(tableId) + } + } + } + + ContentState.prototype.selectTable = function (table) { + // For calculateSelectedCells + this.cellSelectInfo = { + anchor: { + row: 0, + column: 0 + }, + focus: { + row: table.row, + column: table.column + }, + cells: getAllTableCells(table.key) + } + this.calculateSelectedCells() + this.selectedTableCells = { + tableId: table.key, + row: table.row + 1, + column: table.column + 1, + cells: this.cellSelectInfo.selectedCells.map(c => { + delete c.ele + return c + }) + } + // reset cellSelectInfo + this.cellSelectInfo = null + this.muya.blur() + return this.singleRender(table, false) + } + + // Return the cell block if yes, else return null. + ContentState.prototype.isSingleCellSelected = function () { + const { selectedTableCells } = this + if (selectedTableCells && selectedTableCells.cells.length === 1) { + const key = selectedTableCells.cells[0].key + return this.getBlock(key) + } + + return null + } + + // Return the cell block if yes, else return null. + ContentState.prototype.isWholeTableSelected = function () { + const { selectedTableCells } = this + const table = selectedTableCells ? this.getBlock(selectedTableCells.tableId) : {} + const { row, column } = table + if (selectedTableCells && table && selectedTableCells.cells.length === (+row + 1) * (+column + 1)) { + return table + } + + return null + } +} + +export default tableSelectCellsCtrl diff --git a/src/muya/lib/contentState/updateCtrl.js b/src/muya/lib/contentState/updateCtrl.js index 3d76085c..82bae423 100644 --- a/src/muya/lib/contentState/updateCtrl.js +++ b/src/muya/lib/contentState/updateCtrl.js @@ -67,8 +67,12 @@ const updateCtrl = ContentState => { */ ContentState.prototype.checkInlineUpdate = function (block) { // table cell can not have blocks in it - if (/th|td|figure/.test(block.type)) return false - if (/codeContent|languageInput/.test(block.functionType)) return false + if (/figure/.test(block.type)) { + return false + } + if (/cellContent|codeContent|languageInput/.test(block.functionType)) { + return false + } let line = null const { text } = block diff --git a/src/muya/lib/eventHandler/clickEvent.js b/src/muya/lib/eventHandler/clickEvent.js index 1be62231..24afcd8b 100644 --- a/src/muya/lib/eventHandler/clickEvent.js +++ b/src/muya/lib/eventHandler/clickEvent.js @@ -66,6 +66,7 @@ class ClickEvent { // handler table click const toolItem = getToolItem(target) contentState.selectedImage = null + contentState.selectedTableCells = null if (toolItem) { event.preventDefault() event.stopPropagation() @@ -75,7 +76,26 @@ class ClickEvent { contentState.tableToolBarClick(type) } } - // Handler image and inline math preview click + // Handle table drag bar click + if (target.classList.contains('ag-drag-handler')) { + event.preventDefault() + event.stopPropagation() + const rect = target.getBoundingClientRect() + const reference = { + getBoundingClientRect () { + return rect + }, + width: rect.offsetWidth, + height: rect.offsetHeight + } + eventCenter.dispatch('muya-table-bar', { + reference, + tableInfo: { + barType: target.classList.contains('left') ? 'left' : 'bottom' + } + }) + } + // Handle image and inline math preview click const markedImageText = target.previousElementSibling const mathRender = target.closest(`.${CLASS_OR_ID.AG_MATH_RENDER}`) const rubyRender = target.closest(`.${CLASS_OR_ID.AG_RUBY_RENDER}`) diff --git a/src/muya/lib/eventHandler/clipboard.js b/src/muya/lib/eventHandler/clipboard.js index 9773d6ee..dc67c602 100644 --- a/src/muya/lib/eventHandler/clipboard.js +++ b/src/muya/lib/eventHandler/clipboard.js @@ -3,6 +3,7 @@ class Clipboard { this.muya = muya this._copyType = 'normal' // `normal` or `copyAsMarkdown` or `copyAsHtml` this._pasteType = 'normal' // `normal` or `pasteAsPlainText` + this._copyInfo = null this.listen() } @@ -11,8 +12,16 @@ class Clipboard { const docPasteHandler = event => { contentState.docPasteHandler(event) } + const docCopyCutHandler = event => { + contentState.docCopyHandler(event) + if (event.type === 'cut') { + // when user use `cut` function, the dom has been deleted by default. + // But should update content state manually. + contentState.docCutHandler(event) + } + } const copyCutHandler = event => { - contentState.copyHandler(event, this._copyType) + contentState.copyHandler(event, this._copyType, this._copyInfo) if (event.type === 'cut') { // when user use `cut` function, the dom has been deleted by default. // But should update content state manually. @@ -30,6 +39,8 @@ class Clipboard { eventCenter.attachDOMEvent(container, 'paste', pasteHandler) eventCenter.attachDOMEvent(container, 'cut', copyCutHandler) eventCenter.attachDOMEvent(container, 'copy', copyCutHandler) + eventCenter.attachDOMEvent(document.body, 'cut', docCopyCutHandler) + eventCenter.attachDOMEvent(document.body, 'copy', docCopyCutHandler) } copyAsMarkdown () { @@ -47,15 +58,14 @@ class Clipboard { document.execCommand('paste') } - copy (name) { - switch (name) { - case 'table': - this._copyType = 'copyTable' - document.execCommand('copy') - break - default: - break - } + /** + * Copy the anchor block(table, paragraph, math block etc) with the info + * @param {string|object} info is the block key if it's string, or block if it's object + */ + copy (info) { + this._copyType = 'copyBlock' + this._copyInfo = info + document.execCommand('copy') } } diff --git a/src/muya/lib/eventHandler/keyboard.js b/src/muya/lib/eventHandler/keyboard.js index 2447c8b3..c4462cf8 100644 --- a/src/muya/lib/eventHandler/keyboard.js +++ b/src/muya/lib/eventHandler/keyboard.js @@ -115,6 +115,9 @@ class Keyboard { case EVENT_KEYS.Backspace: { return contentState.docBackspaceHandler(event) } + case EVENT_KEYS.Delete: { + return contentState.docDeleteHandler(event) + } case EVENT_KEYS.ArrowUp: // fallthrough case EVENT_KEYS.ArrowDown: // fallthrough case EVENT_KEYS.ArrowLeft: // fallthrough @@ -127,6 +130,7 @@ class Keyboard { if (event.metaKey || event.ctrlKey) { container.classList.add('ag-meta-or-ctrl') } + if ( this.shownFloat.size > 0 && ( @@ -172,11 +176,6 @@ class Keyboard { this.muya.dispatchChange() } break - case 'a': - if (event.ctrlKey) { - contentState.tableCellHandler(event) - } - break case EVENT_KEYS.ArrowUp: // fallthrough case EVENT_KEYS.ArrowDown: // fallthrough case EVENT_KEYS.ArrowLeft: // fallthrough diff --git a/src/muya/lib/eventHandler/mouseEvent.js b/src/muya/lib/eventHandler/mouseEvent.js index d518d710..edf98193 100644 --- a/src/muya/lib/eventHandler/mouseEvent.js +++ b/src/muya/lib/eventHandler/mouseEvent.js @@ -4,6 +4,7 @@ class MouseEvent { constructor (muya) { this.muya = muya this.mouseBinding() + this.mouseDown() } mouseBinding () { @@ -39,6 +40,19 @@ class MouseEvent { eventCenter.attachDOMEvent(container, 'mouseover', handler) eventCenter.attachDOMEvent(container, 'mouseout', leaveHandler) } + + mouseDown () { + const { container, eventCenter, contentState } = this.muya + const handler = event => { + const target = event.target + if (target.classList && target.classList.contains('ag-drag-handler')) { + contentState.handleMouseDown(event) + } else if (target && target.closest('tr')) { + contentState.handleCellMouseDown(event) + } + } + eventCenter.attachDOMEvent(container, 'mousedown', handler) + } } export default MouseEvent diff --git a/src/muya/lib/index.js b/src/muya/lib/index.js index 34dd6886..85779bb4 100644 --- a/src/muya/lib/index.js +++ b/src/muya/lib/index.js @@ -262,6 +262,9 @@ class Muya { } blur () { + const selection = document.getSelection() + + selection.removeAllRanges() this.container.blur() } @@ -321,12 +324,13 @@ class Muya { } selectAll () { - if (this.hasFocus()) { - this.contentState.selectAll() - } - const activeElement = document.activeElement - if (activeElement.nodeName === 'INPUT') { - activeElement.select() + this.contentState.selectAll() + + if (!this.hasFocus()) { + const activeElement = document.activeElement + if (activeElement.nodeName === 'INPUT') { + activeElement.select() + } } } @@ -342,8 +346,12 @@ class Muya { this.clipboard.pasteAsPlainText() } - copy (name) { - this.clipboard.copy(name) + /** + * Copy the anchor block contains the block with `info`. like copy as markdown. + * @param {string|object} key the block key or block + */ + copy (info) { + return this.clipboard.copy(info) } setOptions (options, needRender = false) { diff --git a/src/muya/lib/parser/render/index.js b/src/muya/lib/parser/render/index.js index 3b3a11fa..155ecd7d 100644 --- a/src/muya/lib/parser/render/index.js +++ b/src/muya/lib/parser/render/index.js @@ -149,7 +149,7 @@ class StateRender { render (blocks, activeBlocks, matches) { const selector = `div#${CLASS_OR_ID.AG_EDITOR_ID}` const children = blocks.map(block => { - return this.renderBlock(block, activeBlocks, matches, true) + return this.renderBlock(null, block, activeBlocks, matches, true) }) const newVdom = h(selector, children) @@ -167,7 +167,7 @@ class StateRender { const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1] // If cursor is not in render blocks, need to render cursor block independently const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1 - const newVnode = h('section', blocks.map(block => this.renderBlock(block, activeBlocks, matches))) + const newVnode = h('section', blocks.map(block => this.renderBlock(null, block, activeBlocks, matches))) const html = toHTML(newVnode).replace(/^
([\s\S]+?)<\/section>$/, '$1') const needToRemoved = [] @@ -196,7 +196,7 @@ class StateRender { const cursorDom = document.querySelector(`#${key}`) if (cursorDom) { const oldCursorVnode = toVNode(cursorDom) - const newCursorVnode = this.renderBlock(cursorOutMostBlock, activeBlocks, matches) + const newCursorVnode = this.renderBlock(null, cursorOutMostBlock, activeBlocks, matches) patch(oldCursorVnode, newCursorVnode) } } @@ -215,7 +215,7 @@ class StateRender { */ singleRender (block, activeBlocks, matches) { const selector = `#${block.key}` - const newVdom = this.renderBlock(block, activeBlocks, matches, true) + const newVdom = this.renderBlock(null, block, activeBlocks, matches, true) const rootDom = document.querySelector(selector) const oldVdom = toVNode(rootDom) patch(oldVdom, newVdom) diff --git a/src/muya/lib/parser/render/renderBlock/renderBlock.js b/src/muya/lib/parser/render/renderBlock/renderBlock.js index 8fdab50a..32aebd33 100644 --- a/src/muya/lib/parser/render/renderBlock/renderBlock.js +++ b/src/muya/lib/parser/render/renderBlock/renderBlock.js @@ -1,10 +1,10 @@ /** * [renderBlock render one block, no matter it is a container block or text block] */ -export default function renderBlock (block, activeBlocks, matches, useCache = false) { +export default function renderBlock (parent, block, activeBlocks, matches, useCache = false) { const method = Array.isArray(block.children) && block.children.length > 0 ? 'renderContainerBlock' : 'renderLeafBlock' - return this[method](block, activeBlocks, matches, useCache) + return this[method](parent, block, activeBlocks, matches, useCache) } diff --git a/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js b/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js index 438d7000..6437df0c 100644 --- a/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js +++ b/src/muya/lib/parser/render/renderBlock/renderContainerBlock.js @@ -1,6 +1,7 @@ import { CLASS_OR_ID } from '../../../config' import { renderTableTools } from './renderToolBar' import { renderEditIcon } from './renderContainerEditIcon' +import { renderLeftBar, renderBottomBar } from './renderTableDargBar' import { h } from '../snabbdom' const PRE_BLOCK_HASH = { @@ -15,9 +16,11 @@ const PRE_BLOCK_HASH = { 'vega-lite': `.${CLASS_OR_ID.AG_VEGA_LITE}` } -export default function renderContainerBlock (block, activeBlocks, matches, useCache = false) { +export default function renderContainerBlock (parent, block, activeBlocks, matches, useCache = false) { let selector = this.getSelector(block, activeBlocks) const { + key, + align, type, headingStyle, editable, @@ -26,9 +29,10 @@ export default function renderContainerBlock (block, activeBlocks, matches, useC listItemType, bulletMarkerOrDelimiter, isLooseListItem, - lang + lang, + column } = block - const children = block.children.map(child => this.renderBlock(child, activeBlocks, matches, useCache)) + const children = block.children.map(child => this.renderBlock(block, child, activeBlocks, matches, useCache)) const data = { attrs: {}, dataset: {} @@ -42,7 +46,66 @@ export default function renderContainerBlock (block, activeBlocks, matches, useC selector += `.language-${lang.replace(/[#.]{1}/g, '')}` } - if (/^h/.test(type)) { + if (/th|td/.test(type)) { + const { cells } = this.muya.contentState.selectedTableCells || {} + if (cells && cells.length) { + const cell = cells.find(c => c.key === key) + if (cell) { + const { top, right, bottom, left } = cell + selector += '.ag-cell-selected' + if (top) { + selector += '.ag-cell-border-top' + } + if (right) { + selector += '.ag-cell-border-right' + } + if (bottom) { + selector += '.ag-cell-border-bottom' + } + if (left) { + selector += '.ag-cell-border-left' + } + } + } + } + + // Judge whether to render the table drag bar. + const { cells } = this.muya.contentState.selectedTableCells || {} + if (/th|td/.test(type) && (!cells || cells && cells.length === 0)) { + const table = this.muya.contentState.closest(block, 'table') + const findTable = activeBlocks.find(b => b.key === table.key) + if (findTable) { + const { row: tableRow, column: tableColumn } = findTable + const isLastRow = () => { + const rowContainer = this.muya.contentState.closest(block, /tbody|thead/) + if (rowContainer.type === 'thead') { + return tableRow === 0 + } else { + return !parent.nextSibling + } + } + if (block.parent === activeBlocks[1].parent && !block.preSibling && tableRow > 0) { + children.unshift(renderLeftBar()) + } + + if (column === activeBlocks[1].column && isLastRow() && tableColumn > 0) { + children.push(renderBottomBar()) + } + } + } + + if (/th|td/.test(type)) { + if (align) { + Object.assign(data.attrs, { + style: `text-align:${align}` + }) + } + if (typeof column === 'number') { + Object.assign(data.dataset, { + column + }) + } + } else if (/^h/.test(type)) { if (/^h\d$/.test(type)) { // TODO: This should be the best place to create and update the TOC. // Cache `block.key` and title and update only if necessary. diff --git a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js index a6c93429..34d3b6eb 100644 --- a/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js +++ b/src/muya/lib/parser/render/renderBlock/renderLeafBlock.js @@ -68,7 +68,7 @@ const hasReferenceToken = tokens => { return result } -export default function renderLeafBlock (block, activeBlocks, matches, useCache = false) { +export default function renderLeafBlock (parent, block, activeBlocks, matches, useCache = false) { const { loadMathMap } = this const { cursor } = this.muya.contentState let selector = this.getSelector(block, activeBlocks) @@ -77,7 +77,6 @@ export default function renderLeafBlock (block, activeBlocks, matches, useCache const { text, type, - align, checked, key, lang, @@ -120,11 +119,7 @@ export default function renderLeafBlock (block, activeBlocks, matches, useCache }) } - if (/th|td/.test(type) && align) { - Object.assign(data.attrs, { - style: `text-align:${align}` - }) - } else if (type === 'div') { + if (type === 'div') { const code = this.codeCache.get(block.preSibling) switch (functionType) { case 'html': { diff --git a/src/muya/lib/parser/render/renderBlock/renderTableDargBar.js b/src/muya/lib/parser/render/renderBlock/renderTableDargBar.js new file mode 100644 index 00000000..bb58ae5e --- /dev/null +++ b/src/muya/lib/parser/render/renderBlock/renderTableDargBar.js @@ -0,0 +1,13 @@ +import { h } from '../snabbdom' + +export const renderLeftBar = () => { + return h('span.ag-drag-handler.left', { + attrs: { contenteditable: 'false' } + }) +} + +export const renderBottomBar = () => { + return h('span.ag-drag-handler.bottom', { + attrs: { contenteditable: 'false' } + }) +} diff --git a/src/muya/lib/parser/render/renderBlock/renderToolBar.js b/src/muya/lib/parser/render/renderBlock/renderToolBar.js index 63d61cef..4b447673 100644 --- a/src/muya/lib/parser/render/renderBlock/renderToolBar.js +++ b/src/muya/lib/parser/render/renderBlock/renderToolBar.js @@ -5,7 +5,7 @@ import TableIcon from '../../../assets/pngicon/table/table@2x.png' import AlignLeftIcon from '../../../assets/pngicon/algin_left/2.png' import AlignRightIcon from '../../../assets/pngicon/algin_right/2.png' import AlignCenterIcon from '../../../assets/pngicon/algin_center/2.png' -import DeleteIcon from '../../../assets/pngicon/delete/2.png' +import DeleteIcon from '../../../assets/pngicon/table_delete/2.png' export const TABLE_TOOLS = [{ label: 'table', @@ -32,7 +32,7 @@ export const TABLE_TOOLS = [{ const renderToolBar = (type, tools, activeBlocks) => { const children = tools.map(tool => { const { label, title, icon } = tool - const { align } = activeBlocks[0] + const { align } = activeBlocks[1] // activeBlocks[0] is span block. cell content. let selector = 'li' if (align && label === align) { selector += '.active' diff --git a/src/muya/lib/selection/index.js b/src/muya/lib/selection/index.js index c59d25b2..0acaf8da 100644 --- a/src/muya/lib/selection/index.js +++ b/src/muya/lib/selection/index.js @@ -497,9 +497,8 @@ class Selection { if (node.nodeType === 3) { node = node.parentNode } - return node.closest('span.ag-paragraph') || - node.closest('th.ag-paragraph') || - node.closest('td.ag-paragraph') + + return node.closest('span.ag-paragraph') } getCursorRange () { diff --git a/src/muya/lib/ui/tablePicker/index.js b/src/muya/lib/ui/tablePicker/index.js index 45e0b4e8..7a08324c 100644 --- a/src/muya/lib/ui/tablePicker/index.js +++ b/src/muya/lib/ui/tablePicker/index.js @@ -149,7 +149,7 @@ class TablePicker extends BaseFloat { selectItem () { const { cb } = this const { row, column } = this.select - cb(Math.max(row, 1), Math.max(column, 1)) + cb(Math.max(row, 0), Math.max(column, 0)) this.hide() } } diff --git a/src/muya/lib/ui/tableTools/config.js b/src/muya/lib/ui/tableTools/config.js new file mode 100644 index 00000000..2b7e5ab6 --- /dev/null +++ b/src/muya/lib/ui/tableTools/config.js @@ -0,0 +1,34 @@ +export const toolList = { + left: [{ + label: 'Insert Row Above', + action: 'insert', + location: 'previous', + target: 'row' + }, { + label: 'Insert Row Below', + action: 'insert', + location: 'next', + target: 'row' + }, { + label: 'Remove Row', + action: 'remove', + location: 'current', + target: 'row' + }], + bottom: [{ + label: 'Insert Column Left', + action: 'insert', + location: 'left', + target: 'column' + }, { + label: 'Insert Column Right', + action: 'insert', + location: 'right', + target: 'column' + }, { + label: 'Remove Column', + action: 'remove', + location: 'current', + target: 'column' + }] +} diff --git a/src/muya/lib/ui/tableTools/index.css b/src/muya/lib/ui/tableTools/index.css new file mode 100644 index 00000000..64b9b9be --- /dev/null +++ b/src/muya/lib/ui/tableTools/index.css @@ -0,0 +1,41 @@ +.ag-table-bar-tools { + width: 150px; +} +.ag-table-bar-tools ul, +.ag-table-bar-tools li { + margin: 0; + padding: 0; +} + +.ag-table-bar-tools ul { + padding: 5px 0; +} + +.ag-table-bar-tools li.item { + height: 25px; + line-height: 25px; + color: var(--editorColor); + padding: 0 8px; + cursor: pointer; + position: relative; + font-size: 13px; +} + +.ag-table-bar-tools li.item[data-label=remove] { + margin-top: 10px; +} + +.ag-table-bar-tools li.item[data-label=remove]::before { + content: ''; + width: calc(100% - 16px); + position: absolute; + height: 1px; + background: var(--editorColor04); + top: -5px; + display: block; + left: 8px; +} + +.ag-table-bar-tools li.item:hover { + background-color: var(--floatHoverColor); +} diff --git a/src/muya/lib/ui/tableTools/index.js b/src/muya/lib/ui/tableTools/index.js new file mode 100644 index 00000000..cab5bc95 --- /dev/null +++ b/src/muya/lib/ui/tableTools/index.js @@ -0,0 +1,86 @@ +import BaseFloat from '../baseFloat' +import { patch, h } from '../../parser/render/snabbdom' +import { toolList } from './config' + +import './index.css' + +const defaultOptions = { + placement: 'right-start', + modifiers: { + offset: { + offset: '0, 5' + } + }, + showArrow: false +} + +class TableBarTools extends BaseFloat { + static pluginName = 'tableBarTools' + + constructor (muya, options = {}) { + const name = 'ag-table-bar-tools' + const opts = Object.assign({}, defaultOptions, options) + super(muya, name, opts) + this.options = opts + this.oldVnode = null + this.tableInfo = null + this.floatBox.classList.add('ag-table-bar-tools') + const tableBarContainer = this.tableBarContainer = document.createElement('div') + this.container.appendChild(tableBarContainer) + this.listen() + } + + listen () { + super.listen() + const { eventCenter } = this.muya + eventCenter.subscribe('muya-table-bar', ({ reference, tableInfo }) => { + if (reference) { + this.tableInfo = tableInfo + this.show(reference) + this.render() + } else { + this.hide() + } + }) + } + + render () { + const { tableInfo, oldVnode, tableBarContainer } = this + const renderArray = toolList[tableInfo.barType] + const children = renderArray.map((item) => { + const { label } = item + + const selector = 'li.item' + return h(selector, { + dataset: { + label: item.action + }, + on: { + click: event => { + this.selectItem(event, item) + } + } + }, label) + }) + + const vnode = h('ul', children) + + if (oldVnode) { + patch(oldVnode, vnode) + } else { + patch(tableBarContainer, vnode) + } + this.oldVnode = vnode + } + + selectItem (event, item) { + event.preventDefault() + event.stopPropagation() + + const { contentState } = this.muya + contentState.editTable(item) + this.hide() + } +} + +export default TableBarTools diff --git a/src/muya/lib/ui/tooltip/index.js b/src/muya/lib/ui/tooltip/index.js index e3e2b354..505be434 100644 --- a/src/muya/lib/ui/tooltip/index.js +++ b/src/muya/lib/ui/tooltip/index.js @@ -5,7 +5,7 @@ const position = (source, ele) => { const { top, right, height } = rect Object.assign(ele.style, { - top: `${top + height + 20}px`, + top: `${top + height + 15}px`, left: `${right - ele.offsetWidth / 2 - 10}px` }) } diff --git a/src/muya/lib/utils/exportMarkdown.js b/src/muya/lib/utils/exportMarkdown.js index fb672cb3..ca26e1e5 100644 --- a/src/muya/lib/utils/exportMarkdown.js +++ b/src/muya/lib/utils/exportMarkdown.js @@ -285,10 +285,10 @@ class ExportMarkdown { return str.replace(/([^\\])\|/g, '$1\\|') } - tableData.push(tHeader.children[0].children.map(th => escapeText(th.text).trim())) + tableData.push(tHeader.children[0].children.map(th => escapeText(th.children[0].text).trim())) if (tBody) { tBody.children.forEach(bodyRow => { - tableData.push(bodyRow.children.map(td => escapeText(td.text).trim())) + tableData.push(bodyRow.children.map(td => escapeText(td.children[0].text).trim())) }) } diff --git a/src/muya/lib/utils/importMarkdown.js b/src/muya/lib/utils/importMarkdown.js index ffd389c9..83124c10 100644 --- a/src/muya/lib/utils/importMarkdown.js +++ b/src/muya/lib/utils/importMarkdown.js @@ -216,33 +216,53 @@ const importRegister = ContentState => { // We have to re-escape the chraracter to not break the table. return text.replace(/\|/g, '\\|') } - for (const headText of header) { - const i = header.indexOf(headText) + let i + let j + const headerLen = header.length + for (i = 0; i < headerLen; i++) { + const headText = header[i] const th = this.createBlock('th', { - text: restoreTableEscapeCharacters(headText) + align: align[i] || '', + column: i }) - Object.assign(th, { align: align[i] || '', column: i }) + const cellContent = this.createBlock('span', { + text: restoreTableEscapeCharacters(headText), + functionType: 'cellContent' + }) + this.appendChild(th, cellContent) this.appendChild(theadRow, th) } - for (const row of cells) { + const rowLen = cells.length + for (i = 0; i < rowLen; i++) { const rowBlock = this.createBlock('tr') - for (const cell of row) { - const i = row.indexOf(cell) + const rowContents = cells[i] + const colLen = rowContents.length + for (j = 0; j < colLen; j++) { + const cell = rowContents[j] const td = this.createBlock('td', { - text: restoreTableEscapeCharacters(cell) + align: align[j] || '', + column: j }) - Object.assign(td, { align: align[i] || '', column: i }) + const cellContent = this.createBlock('span', { + text: restoreTableEscapeCharacters(cell), + functionType: 'cellContent' + }) + + this.appendChild(td, cellContent) this.appendChild(rowBlock, td) } this.appendChild(tbody, rowBlock) } + Object.assign(table, { row: cells.length, column: header.length - 1 }) // set row and column block = this.createBlock('figure') block.functionType = 'table' this.appendChild(thead, theadRow) this.appendChild(block, table) this.appendChild(table, thead) - this.appendChild(table, tbody) + if (tbody.children.length) { + this.appendChild(table, tbody) + } this.appendChild(parentList[0], block) break } @@ -358,8 +378,8 @@ const importRegister = ContentState => { html = html.replace(/ <\/span>/g, String.fromCharCode(160)) html = turnSoftBreakToSpan(html) - const markdown = turndownService.turndown(html) + return markdown } diff --git a/src/muya/themes/default.css b/src/muya/themes/default.css index 31ead666..e3bb6e28 100644 --- a/src/muya/themes/default.css +++ b/src/muya/themes/default.css @@ -385,24 +385,39 @@ kbd { padding: 0; } - table thead tr, + /* table thead tr, table tr:nth-child(2n) { background-color: var(--editorColor04); - } + } */ table tr th { font-weight: bold; - border: 1px solid var(--tableBorderColor); text-align: left; margin: 0; padding: 6px 13px; + position: relative; } table tr td { - border: 1px solid var(--tableBorderColor); text-align: left; margin: 0; padding: 6px 13px; + position: relative; + } + + table tr th::before, + table tr td::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + width: calc(100% + 1px); + height: calc(100% + 1px); + border: 1px solid var(--tableBorderColor); + box-sizing: border-box; + pointer-events: none; } table tr th:first-child, @@ -415,6 +430,119 @@ kbd { margin-bottom: 0; } + table tr .ag-drag-handler.left { + position: absolute; + top: 50%; + transform: translateY(-50%); + display: block; + width: 9px; + height: 19px; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid var(--iconColor); + left: -11px; + cursor: pointer; + opacity: .5; + } + + table tr .ag-drag-handler.left::before, + table tr .ag-drag-handler.left::after { + content: ''; + display: block; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--iconColor); + position: absolute; + left: 2px; + top: 4px; + } + table tr .ag-drag-handler.left::after { + top: 10px; + } + + + table tr .ag-drag-handler.bottom { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: block; + width: 19px; + height: 9px; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid var(--iconColor); + bottom: -15px; + cursor: pointer; + opacity: .5; + } + + table tr .ag-drag-handler.bottom::before, + table tr .ag-drag-handler.bottom::after { + content: ''; + display: block; + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--iconColor); + position: absolute; + top: 2px; + left: 4px; + } + table tr .ag-drag-handler.bottom::after { + left: 10px; + } + + table tr .ag-drag-handler.left, + table tr .ag-drag-handler.bottom + { + display: none; + } + + table.ag-active tr .ag-drag-handler { + display: block; + } + + table .ag-drag-cell { + opacity: .7; + z-index: 1; + background: var(--floatBgColor); + transition: left .3s ease-in-out, bottom .3s ease-in-out; + } + + table .ag-drag-cell.ag-drag-left { + left: -6px; + } + + table .ag-drag-cell.ag-drag-bottom { + bottom: -6px; + } + + table .ag-cell-transform { + transition: transform .3s ease-in-out; + } + + table .ag-cell-selected::before { + background: var(--editorColor04); + z-index: 1; + } + + table .ag-cell-border-top::before { + border-top-color: var(--themeColor); + } + + table .ag-cell-border-right::before { + border-right-color: var(--themeColor); + } + + table .ag-cell-border-bottom::before { + border-bottom-color: var(--themeColor); + } + + table .ag-cell-border-left::before { + border-left-color: var(--themeColor); + } + span code, td code, th code { diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index 76fcd4ea..bb536b67 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -86,6 +86,7 @@ import ImageToolbar from 'muya/lib/ui/imageToolbar' import Transformer from 'muya/lib/ui/transformer' import FormatPicker from 'muya/lib/ui/formatPicker' import LinkTools from 'muya/lib/ui/linkTools' +import TableBarTools from 'muya/lib/ui/tableTools' import FrontMenu from 'muya/lib/ui/frontMenu' import bus from '../../bus' import Search from '../search' @@ -362,6 +363,7 @@ export default { Muya.use(LinkTools, { jumpClick: this.jumpClick }) + Muya.use(TableBarTools) const options = { focusMode, @@ -424,9 +426,7 @@ export default { bus.$on('createParagraph', this.handleParagraph) bus.$on('deleteParagraph', this.handleParagraph) bus.$on('insertParagraph', this.handleInsertParagraph) - bus.$on('editTable', this.handleEditTable) bus.$on('scroll-to-header', this.scrollToHeader) - bus.$on('copy-block', this.handleCopyBlock) bus.$on('print', this.handlePrint) bus.$on('screenshot-captured', this.handleScreenShot) @@ -753,19 +753,10 @@ export default { editor && editor.insertParagraph(location) }, - handleEditTable (data) { - const { editor } = this - editor && editor.editTable(data) - }, - blurEditor () { this.editor.blur() }, - handleCopyBlock (name) { - this.editor.copy(name) - }, - handleScreenShot () { if (this.editor) { document.execCommand('paste') @@ -795,9 +786,7 @@ export default { bus.$off('createParagraph', this.handleParagraph) bus.$off('deleteParagraph', this.handleParagraph) bus.$off('insertParagraph', this.handleInsertParagraph) - bus.$off('editTable', this.handleEditTable) bus.$off('scroll-to-header', this.scrollToHeader) - bus.$off('copy-block', this.handleCopyBlock) bus.$off('print', this.handlePrint) bus.$off('screenshot-captured', this.handleScreenShot) diff --git a/src/renderer/contextMenu/editor/actions.js b/src/renderer/contextMenu/editor/actions.js index 915277de..d2cdc6af 100644 --- a/src/renderer/contextMenu/editor/actions.js +++ b/src/renderer/contextMenu/editor/actions.js @@ -1,9 +1,5 @@ import bus from '../../bus' -export const copyTable = () => { - bus.$emit('copy-block', 'table') -} - export const copyAsMarkdown = (menuItem, browserWindow) => { bus.$emit('copyAsMarkdown', 'copyAsMarkdown') } @@ -19,7 +15,3 @@ export const pasteAsPlainText = (menuItem, browserWindow) => { export const insertParagraph = location => { bus.$emit('insertParagraph', location) } - -export const editTable = data => { - bus.$emit('editTable', data) -} diff --git a/src/renderer/contextMenu/editor/index.js b/src/renderer/contextMenu/editor/index.js index 1b2863b0..c404b0ef 100644 --- a/src/renderer/contextMenu/editor/index.js +++ b/src/renderer/contextMenu/editor/index.js @@ -3,17 +3,12 @@ import { CUT, COPY, PASTE, - COPY_TABLE, COPY_AS_MARKDOWN, COPY_AS_HTML, PASTE_AS_PLAIN_TEXT, SEPARATOR, INSERT_BEFORE, - INSERT_AFTER, - INSERT_ROW, - REMOVE_ROW, - INSERT_COLUMN, - REMOVE_COLUMN + INSERT_AFTER } from './menuItems' const { Menu, MenuItem } = remote @@ -24,19 +19,7 @@ export const showContextMenu = (event, { start, end }) => { const disableCutAndCopy = start.key === end.key && start.offset === end.offset const CONTEXT_ITEMS = [INSERT_BEFORE, INSERT_AFTER, SEPARATOR, CUT, COPY, PASTE, SEPARATOR, COPY_AS_MARKDOWN, COPY_AS_HTML, PASTE_AS_PLAIN_TEXT] - if (/th|td/.test(start.block.type) && start.key === end.key) { - CONTEXT_ITEMS.unshift( - INSERT_ROW, - REMOVE_ROW, - INSERT_COLUMN, - REMOVE_COLUMN, - SEPARATOR, - COPY_TABLE, - SEPARATOR - ) - } - - [CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => { + ;[CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => { item.enabled = !disableCutAndCopy }) diff --git a/src/renderer/contextMenu/editor/menuItems.js b/src/renderer/contextMenu/editor/menuItems.js index e1b51467..14d2e1a0 100644 --- a/src/renderer/contextMenu/editor/menuItems.js +++ b/src/renderer/contextMenu/editor/menuItems.js @@ -18,14 +18,6 @@ export const PASTE = { role: 'paste' } -export const COPY_TABLE = { - label: 'Copy Table', - id: 'copyTableMenuItem', - click (menuItem, browserWindow) { - contextMenu.copyTable() - } -} - export const COPY_AS_MARKDOWN = { label: 'Copy As Markdown', id: 'copyAsMarkdownMenuItem', @@ -66,116 +58,6 @@ export const INSERT_AFTER = { } } -export const INSERT_ROW = { - label: 'Insert Row', - submenu: [{ - label: 'Previous Row', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'previous', - action: 'insert', - target: 'row' - }) - } - }, { - label: 'Next Row', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'next', - action: 'insert', - target: 'row' - }) - } - }] -} - -export const REMOVE_ROW = { - label: 'Remove Row', - submenu: [{ - label: 'Previous Row', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'previous', - action: 'remove', - target: 'row' - }) - } - }, { - label: 'Current Row', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'current', - action: 'remove', - target: 'row' - }) - } - }, { - label: 'Next Row', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'next', - action: 'remove', - target: 'row' - }) - } - }] -} - -export const INSERT_COLUMN = { - label: 'Insert Column', - submenu: [{ - label: 'Left Column', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'left', - action: 'insert', - target: 'column' - }) - } - }, { - label: 'Right Column', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'right', - action: 'insert', - target: 'column' - }) - } - }] -} - -export const REMOVE_COLUMN = { - label: 'Remove Column', - submenu: [{ - label: 'Left Column', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'left', - action: 'remove', - target: 'column' - }) - } - }, { - label: 'Current Column', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'current', - action: 'remove', - target: 'column' - }) - } - }, { - label: 'Right Column', - click (menuItem, browserWindow) { - contextMenu.editTable({ - location: 'right', - action: 'remove', - target: 'column' - }) - } - }] -} - export const SEPARATOR = { type: 'separator' }