Recently, independent selection and manipulation of tables were implemented in the rich text editor of Slate.js. Because the table has the cell merge operation, makes the selection calculation and operation function becomes more complex, so the relevant implementation is recorded. There’s not a lot of slate.js involved, so it doesn’t necessarily limit the technology stack. I hope I can provide you with an idea when meeting similar functional requirements.The whole function content is relatively large, so it will be divided into the following three parts:
- Independent selection of cells and range calculation;
- Cell operation;
- Row and column operations.
This article is to explain the realization of table cell splitting. According to the cellrowSpan/colSpan
Split intorowSpan/colSpan
The cell of 1 will sort out the problems such as whether a new row needs to be inserted, the calculation of insertion position and the splitting of multiple cells in the process of splitting. The effect is as follows:
Cell splitting
Splitting cells is a process of splitting data across rows and columns of table cells or selected cells. It can be divided into the following steps:
- Split calculation: calculate whether the new row needs to be inserted, and insert position calculation (there will be multiple places to insert);
- Cell filling: It is necessary to split cells to modify corresponding attributes, and insert other cells after splitting;
- Split multiple cells.
Split calculation
As mentioned in merging cells, there may be a case of merging whole rows of cells when merging cells, so new rows need to be inserted to deal with the problem of missing rows when splitting cells.
In a split calculation, you need to count the number of missing rows and where the new row is inserted, and there may be multiple places where rows need to be inserted. The calculation process can be understood through the following steps:
- The coordinate range of cells in the source table is obtained by source table data.
- In the source table data, the loop starts from the row below the current cell to get the increment and the row index data in the source table data;
- The difference between the source table row index data of the current row and the increment data is the number of inserted rows; The insertion position (relative to the split cell position) is the sum of the current insertion index and inserted rows;
- Insert rows based on computed results.
// Get the number and position of the inserted row
function getInsertRowInfo(editor: IYTEditor, tablePath: Path, cell: NodeEntry
,
) {
// Get the source table data
const [tableNode] = Editor.node(editor, tablePath)
const originTable = getOriginTable(tableNode as TableElement)
// Gets the start row index of the current cell row in the source table
const [cellNode, cellPath] = cell
const { rowSpan = 1 } = cellNode as TableCellElement
const relativePath = Path.relative(cellPath, tablePath)
const originPath = originTable[relativePath[0]][relativePath[1]]
const currentOriginRow: number = originPath[0] [0]
// Range data in the cell source table
const range: number = originPath[1] [0] - originPath[0] [0]
const positions: [number.number=] [] []let insertRow = 0
for (let i = 1; i <= range; i++) {
const rowIndex = relativePath[0] + i
// Add table rows to index (insert new row will affect index, need to add number of inserted rows)
const originRowIndex = currentOriginRow + i + insertRow
if(! originTable[rowIndex]) {// Insert the data calculation when the next row in the table is the last
positions.push([rowSpan - insertRow - i, i + insertRow])
return positions
}
const newOrigin = originTable[rowIndex][0]
// The row index in the actual source table
const currentRowIndex: number = Array.isArray(newOrigin[0])? newOrigin[0] [0]
: newOrigin[0]
if(currentRowIndex ! == originRowIndex) {// Not equal means row data needs to be inserted
InserRow is added because the previous data inserts affect the position of the following cells
const position: [number.number] = [
currentRowIndex - originRowIndex,
i + insertRow,
]
insertRow += currentRowIndex - originRowIndex
positions.push(position)
}
}
return positions
}
/ / insert row
if (rowSpan > 1) {
// Insert rows are handled only if there are cross-row cells
const insertRowArr = getInsertRowInfo(editor, tablePath, cellNode)
if (insertRowArr.length > 0) {
// Current row index
const [rowIndex] = Path.relative(currentRow, tablePath)
insertRowArr.forEach(([insertRow, position]) = > {
// Insert the number of rows as needed to generate the corresponding data
const rowNodes = Array.from({ length: insertRow }).map(() = >
getRowNode([]),
)
Transforms.insertNodes(editor, rowNodes, {
// Calculate the insertion position. Position takes into account the effect of the previous data insertion
at: [...tablePath, rowIndex + position],
})
})
}
}
Copy the code
Cell filling
After processing the effect of cell splitting on row data, you need to modify the properties of the current cell and insert rowSpan * colSpan -1 cells into the table. It can be divided into the following steps:
- Modify current cell properties;
- Insert the row of the current cell
colSpan - 1
Is located after the current cell. - Insert other rows
colSpan
Cell, the position needs to be calculated.
// Modify the current cell properties
Transforms.setNodes(
editor,
{
colSpan: 1.rowSpan: 1}, {at: cellNode[1],},)// sourceOriginCol is the starting column coordinate in the current cell source table
dealCell(editor, cellNode, sourceOriginCol)
// Insert cells
function dealCell(
editor: IYTEditor,
cellNode: NodeEntry<Node>,
sourceOriginCol: number.) {
let currentRow = Path.parent(cellNode[1])
const { colSpan = 1, rowSpan = 1 } = cellNode[0] as TableCellElement
Array.from({ length: rowSpan }).forEach((_, rowIndex) = > {
if (rowIndex === 0 && colSpan > 1) {
// The current cell line is inserted
const nodes = Array.from({ length: colSpan - 1 }).map(() = >
getEmptyCellNode(),
)
Transforms.insertNodes(editor, nodes, {
at: Path.next(cellNode[1]),})}if(rowIndex ! = =0) {
// The number of existing rows is split
currentRow = Path.next(currentRow)
const nodes = Array.from({ length: colSpan }).map(() = >
getEmptyCellNode(),
)
// Process the start of the split cell in the next row
const path = getNextInsertRowPosition(editor, currentRow, sourceOriginCol)
Transforms.insertNodes(editor, nodes, {
at: path,
})
}
})
}
Copy the code
The insertion position of the next row of data can be divided into two situations: when the inserted column position is 0 or the behavior is empty, the insertion position is directly at the beginning of the row; Otherwise, use the data from the column preceding the current row in the source table to determine whether the corresponding table cell is in the same row (different rows are merged by other rows), and if not, move the column further forward to the beginning of the row.
function getNextInsertRowPosition(
editor: IYTEditor,
nextRow: Path,
sourceOriginCol: number.) {
const [rowNode, rowPath] = Editor.node(editor, nextRow)
const tablePath = Path.parent(rowPath)
if (
Editor.isEmpty(editor, rowNode as TableRowElement) ||
sourceOriginCol === 0
) {
/ / manually inserted row, directly inserted into the first position | | at the beginning of the split
return [...nextRow, 0]}// You need to obtain the insertion position from the source table
let i = 1
const [tableNode] = Editor.node(editor, tablePath)
const originTable = getOriginTable(tableNode as TableElement)
const relativeRowPath = Path.relative(nextRow, tablePath)
const originCell = originTable[relativeRowPath[0]] [0]
const originRow = Array.isArray(originCell[0])? originCell[0] [0]
: originCell[0]
while (true) {
// Need to understand the process
const sourceCellOriginPath = [originRow, sourceOriginCol - i]
const realPath = getRealPathByPath(originTable, sourceCellOriginPath)
if (realPath[0] === relativeRowPath[0]) {
// Exclude not in the current row to avoid cell path beyond the table
return [...nextRow, realPath[1] + 1]}if (sourceOriginCol === i) {
// Finally not found
return [...nextRow, 0]
}
i++
}
}
Copy the code
Multi-cell split
When splitting cells by table selection, you cannot split them by selection order. Because after the preceding split, the subsequent cells are not in the correct position, which is very troublesome to calculate. So it can be split in reverse order without affecting the position of other cells (the code is very simple, just need to think of split in reverse order).
function splitCells(editor: IYTEditor, cellPaths: Path[]) {
const newCell: Path[] = getCellBySelectOrFocus(editor, cellPaths)
if(! newCell[0]) return
// Split the cell in reverse order to avoid finding the corresponding position; [...newCell].reverse().forEach((cell) = > {
const node = Editor.node(editor, cell)
splitCell(editor, node)
})
// Focus focus
Transforms.select(editor, {
anchor: Editor.end(editor, newCell[0]),
focus: Editor.end(editor, newCell[0]),})}Copy the code
conclusion
The ability of cells to merge freely opens up many possibilities. When splitting, deal with the following issues:
- When rows need to be inserted, the position and number of rows, and there may be multiple inserts;
- Calculation of cell position for each row when inserting cells;
- When splitting multiple cells, it is necessary to switch ideas and split them in reverse order.
Whether cell merge or split, source table data is an important source of data to use and transform accordingly.
Looking forward to
The next article is on table insertion and row functionality. Insert rows and columns is not a simple matter of inserting a new row in a table position. Some special cases need to be handled, such as:
- When inserting columns, you need to add columns to each row. How is the position of each column calculated due to merging?
- What happens when an insert row encounters a merge cell that runs through it?
Solutions to the above problems will be explained in table inserts, please look for……