mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:19:35 +08:00
371 lines
13 KiB
JavaScript
371 lines
13 KiB
JavaScript
import { isLengthEven } from '../utils'
|
|
import { TABLE_TOOLS } from '../config'
|
|
|
|
const TABLE_BLOCK_REG = /^\|.*?(\\*)\|.*?(\\*)\|/
|
|
|
|
const tableBlockCtrl = ContentState => {
|
|
ContentState.prototype.createTable = function ({ rows, columns }, headerTexts) {
|
|
const table = this.createBlock('table')
|
|
const tHead = this.createBlock('thead')
|
|
const tBody = this.createBlock('tbody')
|
|
|
|
this.appendChild(table, tHead)
|
|
this.appendChild(table, 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)
|
|
for (j = 0; j < columns; j++) {
|
|
const cell = this.createBlock(i === 0 ? 'th' : 'td', headerTexts && i === 0 ? headerTexts[j] : '')
|
|
this.appendChild(rowBlock, cell)
|
|
cell.align = ''
|
|
cell.column = j
|
|
}
|
|
}
|
|
return table
|
|
}
|
|
|
|
ContentState.prototype.createFigure = function ({ rows, columns }) {
|
|
const { start, end } = this.cursor
|
|
const toolBar = this.createToolBar(TABLE_TOOLS, 'table')
|
|
const table = this.createTable({ rows, columns })
|
|
let figureBlock
|
|
if (start.key === end.key) {
|
|
const startBlock = this.getBlock(start.key)
|
|
const anchor = startBlock.type === 'span' ? this.getParent(startBlock) : startBlock
|
|
if (startBlock.text) {
|
|
figureBlock = this.createBlock('figure')
|
|
this.insertAfter(figureBlock, anchor)
|
|
} else {
|
|
figureBlock = anchor
|
|
figureBlock.type = 'figure'
|
|
figureBlock.functionType = 'table'
|
|
figureBlock.text = ''
|
|
figureBlock.children = []
|
|
}
|
|
this.appendChild(figureBlock, toolBar)
|
|
this.appendChild(figureBlock, table)
|
|
}
|
|
const key = table.children[0].children[0].children[0].key // fist cell key in thead
|
|
const offset = 0
|
|
this.cursor = {
|
|
start: { key, offset },
|
|
end: { key, offset }
|
|
}
|
|
this.eventCenter.dispatch('stateChange')
|
|
this.partialRender()
|
|
}
|
|
|
|
ContentState.prototype.initTable = function (block) {
|
|
const { text } = block.children[0]
|
|
const rowHeader = []
|
|
const len = text.length
|
|
let i
|
|
for (i = 0; i < len; i++) {
|
|
const char = text[i]
|
|
if (/^[^|]$/.test(char)) {
|
|
rowHeader[rowHeader.length - 1] += char
|
|
}
|
|
if (/\\/.test(char)) {
|
|
rowHeader[rowHeader.length - 1] += text[++i]
|
|
}
|
|
if (/\|/.test(char) && i !== len - 1) {
|
|
rowHeader.push('')
|
|
}
|
|
}
|
|
const columns = rowHeader.length
|
|
const rows = 2
|
|
|
|
const table = this.createTable({ rows, columns }, rowHeader)
|
|
const toolBar = this.createToolBar(TABLE_TOOLS, 'table')
|
|
|
|
block.type = 'figure'
|
|
block.text = ''
|
|
block.children = []
|
|
block.functionType = 'table'
|
|
this.appendChild(block, toolBar)
|
|
this.appendChild(block, table)
|
|
|
|
return table.children[1].children[0].children[0] // first cell 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 table = getTable(block)
|
|
const figure = this.getBlock(table.parent)
|
|
switch (type) {
|
|
case 'left':
|
|
case 'center':
|
|
case 'right': {
|
|
const newAlign = align === type ? '' : type
|
|
table.children.forEach(rowContainer => {
|
|
rowContainer.children.forEach(row => {
|
|
row.children[column].align = newAlign
|
|
})
|
|
})
|
|
this.eventCenter.dispatch('stateChange')
|
|
this.partialRender()
|
|
break
|
|
}
|
|
case 'delete': {
|
|
const newLine = this.createBlock('span')
|
|
figure.children = []
|
|
this.appendChild(figure, newLine)
|
|
figure.type = 'p'
|
|
figure.text = ''
|
|
const key = newLine.key
|
|
const offset = 0
|
|
this.cursor = {
|
|
start: { key, offset },
|
|
end: { key, offset }
|
|
}
|
|
this.eventCenter.dispatch('stateChange')
|
|
this.partialRender()
|
|
break
|
|
}
|
|
case 'table': {
|
|
const { tablePicker } = this
|
|
const figureKey = figure.key
|
|
const tableLable = 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]
|
|
const tHead = table.children[0]
|
|
const headerRow = tHead.children[0]
|
|
const bodyRows = tBody.children
|
|
let i
|
|
if (column > oldColumn) {
|
|
for (i = oldColumn + 1; i <= column; i++) {
|
|
const th = this.createBlock('th')
|
|
th.column = i
|
|
th.align = ''
|
|
this.appendChild(headerRow, th)
|
|
bodyRows.forEach(bodyRow => {
|
|
const td = this.createBlock('td')
|
|
td.column = i
|
|
td.align = ''
|
|
this.appendChild(bodyRow, td)
|
|
})
|
|
}
|
|
} else if (column < oldColumn) {
|
|
const rows = [headerRow, ...bodyRows]
|
|
rows.forEach(row => {
|
|
while (row.children.length > column + 1) {
|
|
const lastChild = row.children[row.children.length - 1]
|
|
this.removeBlock(lastChild)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (row < oldRow) {
|
|
while (tBody.children.length > row) {
|
|
const lastRow = tBody.children[tBody.children.length - 1]
|
|
this.removeBlock(lastRow)
|
|
}
|
|
} else if (row > oldRow) {
|
|
const oneRowInBody = bodyRows[0]
|
|
for (i = oldRow + 1; i <= row; i++) {
|
|
const bodyRow = this.createRow(oneRowInBody)
|
|
this.appendChild(tBody, bodyRow)
|
|
}
|
|
}
|
|
Object.assign(table, { row, column })
|
|
|
|
const cursorBlock = headerRow.children[0]
|
|
const key = cursorBlock.key
|
|
const offset = cursorBlock.text.length
|
|
this.cursor = {
|
|
start: { key, offset },
|
|
end: { key, offset }
|
|
}
|
|
this.eventCenter.dispatch('stateChange')
|
|
this.partialRender()
|
|
}
|
|
|
|
tablePicker.toggle({ row, column }, tableLable, 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)) {
|
|
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 thead = table.children[0]
|
|
const tbody = table.children[1]
|
|
const { column } = table
|
|
const columnIndex = currentRow.children.indexOf(block)
|
|
let cursorBlock
|
|
|
|
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
|
|
}
|
|
|
|
if (target === 'row') {
|
|
if (action === 'insert') {
|
|
let newRow = (location === 'previous' && block.type === 'th')
|
|
? createRow(column, true)
|
|
: createRow(column, false)
|
|
if (location === 'previous') {
|
|
this.insertBefore(newRow, currentRow)
|
|
if (block.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') {
|
|
const firstRow = tbody.children[0]
|
|
this.insertBefore(newRow, firstRow)
|
|
} else {
|
|
this.insertAfter(newRow, currentRow)
|
|
}
|
|
}
|
|
cursorBlock = newRow.children[columnIndex]
|
|
// handle remove row
|
|
} else {
|
|
if (location === 'previous') {
|
|
if (block.type === 'th') return
|
|
if (!currentRow.preSibling) {
|
|
const headRow = thead.children[0]
|
|
if (!currentRow.nextSibling) return
|
|
this.removeBlock(headRow)
|
|
this.removeBlock(currentRow)
|
|
currentRow.children.forEach(cell => (cell.type = 'th'))
|
|
this.appendChild(thead, currentRow)
|
|
} else {
|
|
const preRow = this.getPreSibling(currentRow)
|
|
this.removeBlock(preRow)
|
|
}
|
|
} else if (location === 'current') {
|
|
if (block.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]
|
|
}
|
|
if (block.type === 'td' && (currentRow.preSibling || currentRow.nextSibling)) {
|
|
cursorBlock = (this.getNextSibling(currentRow) || this.getPreSibling(currentRow)).children[columnIndex]
|
|
this.removeBlock(currentRow)
|
|
}
|
|
} else {
|
|
if (block.type === 'th') {
|
|
if (tbody.children.length >= 2) {
|
|
const firstRow = tbody.children[0]
|
|
this.removeBlock(firstRow)
|
|
} else {
|
|
return
|
|
}
|
|
} else {
|
|
const nextRow = this.getNextSibling(currentRow)
|
|
if (nextRow) this.removeBlock(nextRow)
|
|
}
|
|
}
|
|
}
|
|
} else if (target === 'column') {
|
|
if (action === 'insert') {
|
|
[...thead.children, ...tbody.children].forEach(tableRow => {
|
|
const targetCell = tableRow.children[columnIndex]
|
|
const cell = this.createBlock(targetCell.type)
|
|
cell.align = ''
|
|
if (location === 'left') {
|
|
this.insertBefore(cell, targetCell)
|
|
} else {
|
|
this.insertAfter(cell, targetCell)
|
|
}
|
|
tableRow.children.forEach((cell, i) => {
|
|
cell.column = i
|
|
})
|
|
})
|
|
cursorBlock = location === 'left' ? this.getPreSibling(block) : this.getNextSibling(block)
|
|
// handle remove column
|
|
} else {
|
|
if (currentRow.children.length <= 2) return
|
|
[...thead.children, ...tbody.children].forEach(tableRow => {
|
|
const targetCell = tableRow.children[columnIndex]
|
|
const removeCell = location === 'left'
|
|
? this.getPreSibling(targetCell)
|
|
: (location === 'current' ? targetCell : this.getNextSibling(targetCell))
|
|
if (removeCell === block) {
|
|
cursorBlock = this.getNextSibling(block)
|
|
}
|
|
if (removeCell) this.removeBlock(removeCell)
|
|
tableRow.children.forEach((cell, i) => {
|
|
cell.column = i
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
const newColum = thead.children[0].children.length - 1
|
|
const newRow = thead.children.length + tbody.children.length - 1
|
|
Object.assign(table, { row: newRow, column: newColum })
|
|
|
|
if (cursorBlock) {
|
|
const { key } = cursorBlock
|
|
const offset = 0
|
|
this.cursor = { start: { key, offset }, end: { key, offset } }
|
|
} else {
|
|
this.cursor = { start, end }
|
|
}
|
|
|
|
this.partialRender()
|
|
this.eventCenter.dispatch('stateChange')
|
|
}
|
|
|
|
ContentState.prototype.getTableBlock = function () {
|
|
const { start, end } = this.cursor
|
|
const startBlock = this.getBlock(start.key)
|
|
const endBlock = this.getBlock(end.key)
|
|
const startParents = this.getParents(startBlock)
|
|
const endParents = this.getParents(endBlock)
|
|
const affiliation = startParents
|
|
.filter(p => endParents.includes(p))
|
|
|
|
if (affiliation.length) {
|
|
const table = affiliation.find(p => p.type === 'figure')
|
|
return table
|
|
}
|
|
}
|
|
|
|
ContentState.prototype.tableBlockUpdate = function (block) {
|
|
const { type } = block
|
|
if (type !== 'p') return false
|
|
const { text } = block.children[0]
|
|
const match = TABLE_BLOCK_REG.exec(text)
|
|
return (match && isLengthEven(match[1]) && isLengthEven(match[2])) ? this.initTable(block) : false
|
|
}
|
|
}
|
|
|
|
export default tableBlockCtrl
|