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'
}