最近在 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
}
由于表格单元格存在合并情况,也可能整行都合并的情况。所以将表格转换为源表格数据,获取源表格数据的行数以及当前位置行的单元格的源表格数据对应的行,从而获得到插入位置在源表格中对应的行。
// 获取源表格中行数
function getRowNumber(originTable: (number | number[])[][][]) {
const lastRowOriginCell = originTable[originTable.length - 1][0]
const rowIndex = Array.isArray(lastRowOriginCell[0])
? lastRowOriginCell[1][0]
: lastRowOriginCell[0]
return rowIndex + 1
}
// 获取插入位置对应的源表格中行位置
const colNum = getColNumber(tableNode) // 获取列数
const rowNum = getRowNumber(originTable) // 获取行数
const targetIndex = direction === 'above' ? 0 : newCell.length - 1
const targetCell = Path.relative(newCell[targetIndex], tablePath) //当前位置
const targetOriginCell = originTable[targetCell[0]][targetCell[1]]
const addConstant = direction === 'above' ? -1 : 1 // 向上 or 向下
const insertOriginRowIndex =
(Array.isArray(targetOriginCell[0])
? direction === 'above'
? targetOriginCell[0][0]
: targetOriginCell[1][0]
: targetOriginCell[0]) + addConstant
有了源表格数据中行位置以及插入的单元格数之后,就可以进行边界条件判断,并且生成行列数据,以便插入到表格中。
if (direction === 'above' && insertOriginRowIndex === -1) {
// 在首行上方插入一行
const insertRows = getRowNode(
Array.from({ length: colNum }).map(() => getEmptyCellNode()),
)
Transforms.insertNodes(editor, insertRows, {
at: [...tablePath, 0],
})
insertRowIndex = 0 // 表格中插入的位置
} else if (direction === 'below' && insertOriginRowIndex === rowNum) {
// 在尾行下方插入一行
const insertRows = getRowNode(
Array.from({ length: colNum }).map(() => getEmptyCellNode()),
)
Transforms.insertNodes(editor, insertRows, {
at: [...tablePath, tableNode.children.length],
})
insertRowIndex = tableNode.children.length
}
}
非边界位置插入
当插入位置不是在边界位置时,就需要对表格中插入位置中每个单元格位置进行处理。当单元格不是合并单元格时,可直接新行中插入单元格;当单元格是合并单元格,且单元格的位置是合并单元格的底部(向上插入)或顶部(向下插入)时,也可直接新行中插入单元格。
Array.from({ length: colNum }).forEach((_, index) => {
const curCell = getRealPathByPath(originTable, [
insertOriginRowIndex,
index,
])
const curOriginCell = getRangeByOrigin(originTable, [
insertOriginRowIndex,
index,
])
const edgeRowIndex =
direction === 'above' ? curOriginCell[1][0] : curOriginCell[0][0]
if (
!Array.isArray(curOriginCell[0]) ||
edgeRowIndex === insertOriginRowIndex
) {
// 当前单元格非合并单元格 或者 当前单元格为合并单元格底部(上方插入)/顶部(下方插入)
insertCells.push(getEmptyCellNode())
}
})
合并单元格
在插入行中插入单元格时,当单元格不满足以上插入规则时,则表示当前单元格位于合并单元格中且不是边界位置,此时则是需要修改单元格的rowSpan
值,达到插入单元格的效果。由于单元格可能存在多列合并,修改rowSpan
时,则会都进行修改,所以需要进行去重处理,同一个合并单元格只需要执行一次。
Array.from({ length: colNum }).forEach((_, index) => {
const curCell = getRealPathByPath(originTable, [
insertOriginRowIndex,
index,
])
const curOriginCell = getRangeByOrigin(originTable, [
insertOriginRowIndex,
index,
])
const edgeRowIndex =
direction === 'above' ? curOriginCell[1][0] : curOriginCell[0][0]
if (
!Array.isArray(curOriginCell[0]) ||
edgeRowIndex === insertOriginRowIndex
) {
......
} else if (
!updateCellPaths.some((cellPath) => Path.equals(curCell, cellPath))
) {
// 需要修改的合并单元格
const [cellNode] = Editor.node(editor, [...tablePath, ...curCell])
const { rowSpan = 1 } = cellNode as TableCellElement
Transforms.setNodes(
editor,
{
rowSpan: rowSpan + 1,
},
{
at: [...tablePath, ...curCell],
},
)
updateCellPaths.push(curCell)
}
})
总结
以上就是表格中向上/向下插入行的主要实现过程。主要是需要:
- 区分边界情况,边界时可以根据列数直接插入新行;
- 非边界情况时,需要对插入位置行的每个单元格计算判断,看是否时插入单元格还时合并单元格。
具体的实现过程和效果,可查看此项目:demo
编辑器系列
展望
下一篇主要是表格的向左/向右插入列,实现思路与插入行相似。区别在于,插入行可以一整行计算之后直接插入。插入列,就需要对每行的插入位置进行计算处理。 敬请期待......