marktext/src/muya/lib/contentState/tableBlockCtrl.js
2018-07-07 00:10:30 +08:00

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