Optimization of table block (#1456)

* Prepare for drag and drop row and column

* remove regexp th|td

* render drag button

* Feat: support drag and drop row and column of table

* Feat: table bar tools

* remove unnecessary codes

* Feat: support select multiple cells

* Do not show table drag bar when selected cells

* Feat: support delete selected cells content or remove row/column/table

* Feat: select one cell or table when press ctrl + a

* Support select all content

* Remove table tools in context menu

* Feat: support copy paste selected cells as sub table

* Fix: PR issue 1 press tab will not show the table drag bars

* Select one cell and press backspace will cause bug

* Fix: The table drag bar location error when there are tow tables in the editor

* Fix unable copy and paste 1* n or n * 1 table

* Drag any row to the top to editor will cause error.

* Update table resize icon

* Fix: table resize is not work in table tool bar

* Fix: No need to show left drag bar if only one row, and no need to show bottom drag bar if only one column.

* Fix: Create an empty table in source code mode, turn to preview mode, there are more than two drag bars in one table.

* Fix: resize table

* Opti: table is not 100% width now

* Fix drag in one row or column

* Change table delete icon

* Fix: backspace is not work

* Little style opti

* Fix: cmd + enter bug

* Update the table drag bar context menu text

* Handle delete key when select table cells

* remove all unnecessary debug codes

* Feat: support cut selected cells and copy/cut by context menu

* Fix typo

* Rename some methods name

* Fix an issue when drag and drop table drag bar

* fix do not handle cell selection when the context menu shown

* Do not handle select cells when mouse up outside table
This commit is contained in:
Ran Luo 2019-10-13 19:23:00 +08:00 committed by Felix Häusler
parent bcb9d97d61
commit 289b17c015
48 changed files with 1592 additions and 516 deletions

View File

@ -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)

View File

@ -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')
) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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) && /<br\/>.{1}$/.test(startBlock.text)) {
if (startBlock.functionType === 'cellContent' && /<br\/>.{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

View File

@ -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

View File

@ -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) {

View File

@ -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 `<br/>` 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 = '<br/>'
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)
}

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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, '<br/>')
startBlock.text = startBlock.text.substring(0, start.offset) + pendingText + startBlock.text.substring(end.offset)
const { key } = startBlock

View File

@ -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

View File

@ -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()) {

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}`)

View File

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

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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(/^<section>([\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)

View File

@ -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)
}

View File

@ -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.

View File

@ -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': {

View File

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

View File

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

View File

@ -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 () {

View File

@ -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()
}
}

View File

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

View File

@ -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);
}

View File

@ -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

View File

@ -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`
})
}

View File

@ -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()))
})
}

View File

@ -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>&nbsp;<\/span>/g, String.fromCharCode(160))
html = turnSoftBreakToSpan(html)
const markdown = turndownService.turndown(html)
return markdown
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}

View File

@ -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
})

View File

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