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:

  1. Independent selection of cells and range calculation;
  2. Cell operation;
  3. Row and column operations.

This article is to explain the realization of table cell splitting. According to the cellrowSpan/colSpanSplit intorowSpan/colSpanThe 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:

  1. Split calculation: calculate whether the new row needs to be inserted, and insert position calculation (there will be multiple places to insert);
  2. Cell filling: It is necessary to split cells to modify corresponding attributes, and insert other cells after splitting;
  3. 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 cellcolSpan - 1Is located after the current cell.
  • Insert other rowscolSpanCell, 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……