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 calculations – SlateJS editor table – selection
  2. Cell operation;
  3. Row and column operations.

This article is to explain the combined cell implementation of the table. Mainly for the selection of cell combination and the whole line combination, increase the line placeholder and other functions. The effect is as follows:

Cell merge

Merging cells is the process of combining data and content across rows and columns of selected cells in a table. It can be divided into the following steps:

  1. Merge content: Merge cell content;
  2. Merge span: Calculate colSpan and rowSpan for merged cells;
  3. Merge cells: Remove selected cells and insert merged cells;
  4. Missing line processing: Deal with the problem of merging whole lines.

Merge content

Combine the contents of all cells in the table selection and exclude empty cells to avoid unnecessary data display.

function mergeChildren(editor: IYTEditor, cellPaths: Path[]) {
  const newChildren: Element[] = []
  cellPaths.forEach((cellPath) = > {
    const [cellNode] = Editor.node(editor, cellPath)
    
    // Exclude empty cells
    const isEmpty = isEmptyCell(editor, cellNode as TableCellElement)
    if(! isEmpty) newChildren.push(... (cellNodeas TableCellElement).children)
  })

  // Empty cells are excluded. If the selected cells are empty, you need to fill in the data manually
  return newChildren.length > 0
    ? newChildren
    : [
        {
          type: 'paragraph'.children: [{ text: ' '}}],},]/** auxiliary methods **/

// Check whether the cell is empty
export function isEmptyCell(editor: IYTEditor, cellNode: TableCellElement) {
  if (cellNode.children.length > 1) return false
  const content = cellNode.children[0]
  if(content.type ! = ='paragraph') return false
  return Editor.isEmpty(editor, content)
}
Copy the code

Merge span

Calculates rowSpan/colSpan data for the combined cells. Because the selected cells in the table have the same column/row, the data of the merged cells cannot be calculated simply by the sum of the selected cells across rows/columns. You can obtain the correct cross-row/cross-column data from the coordinate range data of the cells in the source table by converting the cell data to the source table cell data.

export function getCellsSpan(editor: IYTEditor, table: TableElement, cellPaths: Path[],) {
  // Get the source table data
  const originTable = getOriginTable(table)
  const tablePath = ReactEditor.findPath(editor, table)
  const ranges: rangeType[] = []

  cellPaths.forEach((cellPath) = > {
    // Get source table cell data
    const cellRelative = Path.relative(cellPath, tablePath)
    const originRange = originTable[cellRelative[0]][cellRelative[1]]

    if (Array.isArray(originRange[0&&])Array.isArray(originRange[1])) {
      ranges.push(originRange[0] as rangeType, originRange[1] as rangeType)
    } else {
      ranges.push(originRange as rangeType)
    }
  })

  const{ xRange, yRange } = getRange(... ranges)return {
    rowSpan: xRange[1] - xRange[0] + 1.colSpan: yRange[1] - yRange[0] + 1,}}Copy the code

Merged cell

After processing the attributes required to merge cells (content, cross-row/cross-column data), select cells need to be removed, and rows need to be removed if there are no cells in the row after removal.

function removeCellByPath(editor: IYTEditor, cellPaths: Path[], tablePath: Path,) {
  Transforms.removeNodes(editor, {
    // The first cell will not be removed, so that the current row will not be removed
    at: tablePath,
    match: (_, path) = >! Path.equals(cellPaths[0], path) &&
      cellPaths.some((cellPath) = > Path.equals(cellPath, path)),
  })
  // Remove empty lines
  Transforms.removeNodes(editor, {
    at: tablePath,
    match: (node) = >
      Element.isElement(node) &&
      node.type === 'tableRow' &&
      !Element.matches((node as TableCellElement).children[0] and {type: 'tableCell',})})// Remove the first cell. The row will remain empty
  Transforms.removeNodes(editor, {
    match: (_, path) = > Path.equals(cellPaths[0], path),
  })
}
Copy the code

After removing the cell, insert the combined cell into the position of the first cell in the selection.

  Transforms.insertNodes(
    editor,
    {
      type: 'tableCell'.colSpan: spans.colSpan,
      rowSpan: spans.rowSpan,
      children, // The contents of the merged cells
    },
    {
      at: cellPaths[0].// The first cell position},)// Focus focus
  Transforms.select(editor, {
    anchor: Editor.end(editor, cellPaths[0]),
    focus: Editor.end(editor, cellPaths[0]})),Copy the code

Short line processing

When all cells of a row or rows are merged by other cells, the row does not exist in the data. The data rendered to the page will be distorted, so you need to calculate if there are missing lines and add blank lines to render.


function TableRow(props: RenderElementProps) {
  const { attributes, children, element } = props

  const editor = useSlate()

  const rowPath = ReactEditor.findPath(editor, element)
  // minRow is normally 1, representing the current row; If the value is greater than 1, blank lines need to be added
  const minRow = getNextRowSpan(editor, rowPath)

  return (
    <>
      <tr {. attributes} className="yt-e-table-row">
        {children}
      </tr>
      {minRow > 1 &&
        Array.from({ length: minRow - 1 }).map((_, index) => (
          <tr key={index} />
        ))}
    </>)}Copy the code

To calculate whether a row is missing after the current row, the difference between the row index of the cell in the next row and the row index of the current cell is calculated using the source table data, or the rowSpan data of the current row cell if the next row does not exist.

export function getNextRowSpan(editor: IYTEditor, rowPath: Path) {
  const tablePath = Path.parent(rowPath)
  const [tableNode] = Editor.node(editor, tablePath)
  const [rowNode] = Editor.node(editor, rowPath)
  // Source table data
  const originTable = getOriginTable(tableNode as TableElement)
  // The first cell in the current row in the source table
  const rowIndex = Path.relative(rowPath, tablePath)
  const originRowRange = originTable[rowIndex[0]] [0]
  // Row index of cells in the source table
  const originRowIndex = Array.isArray(originRowRange[0])? originRowRange[0] [0]
    : originRowRange[0]

  if (originTable[originRow[0] + 1]) {
    // If the next row of data exists
    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
}
Copy the code

conclusion

The main idea of merging cells is to use the source table data to perform related calculations.

  • To calculate the combined SPAN, use the source table data to calculate;
  • When removing a cell, keep the row where the first cell is; otherwise, the merged cell cannot be inserted into the corresponding position.
  • When the entire row is merged, the rendered data is supplemented with blank lines.

Looking forward to

The next article will look at splitting cells, which is more complex to implement than merging cells. Such as:

  • How to calculate the number of new rows when there is a merge of whole rows?
  • How to calculate the insertion position of a new row when there is a cell that spans multiple rows and contains the entire row to be merged (cannot be inserted directly after the current row, affecting the data in the next row)?
  • If multiple cells are selected for splitting, splitting data first will affect the coordinates of subsequent cells. How to handle this?

Solutions to the above problems will be explained in cell splitting, so stay tuned to……