最近在 slate.js 的富文本编辑器中实现了 Table 的独立选区以及操作功能。由于表格存在单元格的合并操作,使得在选区计算和操作功能变得更加的复杂,所以对相关的实现进行了记录。当中,涉及到 slate.js 的内容不是很多,所以并不一定限制技术栈。在遇到类似功能需求时,希望能够为你提供出一种思路。
整个功能内容相对还是比较多,所以将分为以下3个部分讲解:
- 独立选区的单元格和范围计算;
- 单元格操作;
- 行列操作。
本文是对表格的插入列实现进行讲解。主要是对正常列插入、首/尾列位置插入、穿过合并单元格时合并等功能实现。
效果如下:
插入位置
插入位置分为两种情况:
- 若选区存在时,向左插入位置为选区第一列的左边,向右插入位置为选区最后一列的右边(选区第一列、选区最后一列视为当前位置);
- 否则在当前单元格左边插入或者右边插入。
向左/右插入行
插入列主要是对表格相应位置插入新行时对每个单元格插入或合并的过程。主要可分为以下情况:
- 插入单元格;
- 合并单元格。
插入单元格
边界位置插入
当插入位置在表格的第一列向左或者最后一列向右插入时,必定是不存在穿过合并单元格的情况。由于表格单元格存在合并情况,也可能整列都合并的情况。所以将表格转换为源表格数据,获取源表格数据的列数以及当前位置的源表格数据对应的列,从而获得到插入位置在源表格中对应的列。
// 获取原始数据列数
function getColNumber(tableNode: TableElement) {
const rowNode = tableNode.children[0]
let colNum = 0
rowNode.children.forEach((cellNode) => {
const { colSpan = 1 } = cellNode
colNum += colSpan
})
return colNum
}
// 获取插入位置对应的源表格中列位置
const colNum = getColNumber(tableNode)
const targetIndex = direction === 'left' ? 0 : newCell.length - 1
const targetCell = Path.relative(newCell[targetIndex], tablePath)
const targetOriginCell = originTable[targetCell[0]][targetCell[1]]
const addConstant = direction === 'left' ? -1 : 1
const insertOriginColIndex =
(Array.isArray(targetOriginCell[0])
? direction === 'left'
? targetOriginCell[0][1]
: targetOriginCell[1][1]
: (targetOriginCell[1] as number)) + addConstant
有了源表格数据中插入位置、列数,就可以进行边界条件判断。循环每一行,在行首或行尾插入一个单元格,但是如果存在整行合并情况,需要插入多个单元格。
// 每行插入单元格
function insertColByCell(
editor: IYTEditor,
tablePath: Path,
index: number,
cellIndex: number,
) {
// 在 insertNodes 前获取跨行数,避免获取不准确
const rowSpan = getNextRowSpan(editor, [...tablePath, index])
Transforms.insertNodes(editor, getEmptyCellNode(), {
at: [...tablePath, index, cellIndex],
})
if (rowSpan > 1) {
Array.from({ length: rowSpan - 1 }).forEach((_, i) => {
Transforms.insertNodes(editor, getRowNode([getEmptyCellNode()]), {
at: [...tablePath, index + i + 1],
})
})
}
return [...tablePath, index, cellIndex]
}
// 循环行插入单元格
for (let index = len - 1; index >= 0; index--) {
if (direction === 'left' && insertOriginColIndex === -1) {
// 在首列左侧插入列
focusPath = insertColByCell(editor, tablePath, index, 0)
} else if (direction === 'right' && insertOriginColIndex === colNum) {
// 在尾列右侧插入列
focusPath = insertColByCell(
editor,
tablePath,
index,
originTable[index].length,
)
}
}
对于如何计算原始表格中当前行在源表格中整行合并的跨度。可以转换成源表格数据,能够更好的计算。
function getNextRowSpan(editor: IYTEditor, rowPath: Path) {
const tablePath = Path.parent(rowPath)
const [tableNode] = Editor.node(editor, tablePath)
const [rowNode] = Editor.node(editor, rowPath)
const originTable = getOriginTable(tableNode as TableElement)
const originRow = Path.relative(rowPath, tablePath)
const originRowRange = originTable[originRow[0]][0]
const originRowIndex = Array.isArray(originRowRange[0])
? originRowRange[0][0]
: originRowRange[0]
if (originTable[originRow[0] + 1]) {
const originNextRowRange = originTable[originRow[0] + 1][0]
const originNextRowIndex = Array.isArray(originNextRowRange[0])
? originNextRowRange[0][0]
: originNextRowRange[0]
return originNextRowIndex - originRowIndex
}
return (rowNode as TableRowElement).children[0].rowSpan || 1
}
非边界位置插入
在行的非首尾位置插入时,需要判断插入位置是否穿过合并的单元。所以需要获取单元格列边界在源表格中的位置。
const originCell = originTable[index][0]
const originRowIndex = Array.isArray(originCell[0])
? originCell[0][0]
: originCell[0]
const curCell = getRealPathByPath(originTable, [
originRowIndex,
insertOriginColIndex,
])
const curOriginCell = getRangeByOrigin(originTable, [
originRowIndex,
insertOriginColIndex,
])
const edgeColIndex =
direction === 'left' ? curOriginCell[1][1] : curOriginCell[0][1]
如果不是合并单元格,或者插入位置是合并单元格的边界,则直接插入单元格。
if (
!Array.isArray(curOriginCell[0]) ||
edgeColIndex === insertOriginColIndex
) {
const insertPath = getNextInsertRowPosition(
editor,
[...tablePath, index],
insertOriginColIndex,
)
const colIndex = insertPath[insertPath.length - 1]
focusPath = insertColByCell(
editor,
tablePath,
index,
direction === 'left' ? colIndex + 1 : colIndex,
)
}
合并单元格
在逐行插入列时,当单元格不满足以上插入规则时,则表示当前单元格位于合并单元格中且不是边界位置,此时则是需要修改单元格的colSpan值,达到插入单元格的效果。由于单元格可能存在多行合并,修改rowSpan时,则会都进行修改,所以需要进行去重处理,同一个合并单元格只需要执行一次。
for (let index = len - 1; index >= 0; index--) {
if (direction === 'left' && insertOriginColIndex === -1) {
} else if (direction === 'right' && insertOriginColIndex === colNum) {
} else {
const originCell = originTable[index][0]
const originRowIndex = Array.isArray(originCell[0])
? originCell[0][0]
: originCell[0]
const curCell = getRealPathByPath(originTable, [
originRowIndex,
insertOriginColIndex,
])
const curOriginCell = getRangeByOrigin(originTable, [
originRowIndex,
insertOriginColIndex,
])
const edgeColIndex =
direction === 'left' ? curOriginCell[1][1] : curOriginCell[0][1]
if (
!Array.isArray(curOriginCell[0]) ||
edgeColIndex === insertOriginColIndex
) {
...
} else if (
!updateCellPaths.some((cellPath) => Path.equals(curCell, cellPath))
) {
// 需要修改的合并单元格
const [cellNode] = Editor.node(editor, [...tablePath, ...curCell])
const { colSpan = 1 } = cellNode as TableCellElement
Transforms.setNodes(
editor,
{
colSpan: colSpan + 1,
},
{
at: [...tablePath, ...curCell],
},
)
updateCellPaths.push(curCell)
}
}
}
总结
以上就是表格中向左/向右插入列的主要实现过程。主要是需要:
- 区分边界情况,边界时可以每行插入单元格;
- 每行插入单元格时,需要计算当前行是否存在整行,若存在则需要插入新行;
- 非边界情况时,需要对每行插入位置的列计算判断,看是否插入单元格还是合并单元格。
具体的实现过程和效果,可查看此项目:demo
展望
下一篇主要是表格的行列删除功能。也会存在删除的行列中单元格处于合并单元格中,处理删除行列后是空表格等问题。 敬请期待......