2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {TableMapType, TableMapValueType} from './LexicalTableSelection';
10 import type {ElementNode, PointType} from 'lexical';
12 import {$findMatchingParent} from '@lexical/utils';
20 import invariant from 'lexical/shared/invariant';
22 import {InsertTableCommandPayloadHeaders} from '.';
27 TableCellHeaderStates,
29 } from './LexicalTableCellNode';
30 import {$createTableNode, $isTableNode, TableNode} from './LexicalTableNode';
31 import {TableDOMTable} from './LexicalTableObserver';
36 } from './LexicalTableRowNode';
37 import {$isTableSelection} from './LexicalTableSelection';
39 export function $createTableNodeWithDimensions(
42 includeHeaders: InsertTableCommandPayloadHeaders = true,
44 const tableNode = $createTableNode();
46 for (let iRow = 0; iRow < rowCount; iRow++) {
47 const tableRowNode = $createTableRowNode();
49 for (let iColumn = 0; iColumn < columnCount; iColumn++) {
50 let headerState = TableCellHeaderStates.NO_STATUS;
52 if (typeof includeHeaders === 'object') {
53 if (iRow === 0 && includeHeaders.rows) {
54 headerState |= TableCellHeaderStates.ROW;
56 if (iColumn === 0 && includeHeaders.columns) {
57 headerState |= TableCellHeaderStates.COLUMN;
59 } else if (includeHeaders) {
61 headerState |= TableCellHeaderStates.ROW;
64 headerState |= TableCellHeaderStates.COLUMN;
68 const tableCellNode = $createTableCellNode(headerState);
69 const paragraphNode = $createParagraphNode();
70 paragraphNode.append($createTextNode());
71 tableCellNode.append(paragraphNode);
72 tableRowNode.append(tableCellNode);
75 tableNode.append(tableRowNode);
81 export function $getTableCellNodeFromLexicalNode(
82 startingNode: LexicalNode,
83 ): TableCellNode | null {
84 const node = $findMatchingParent(startingNode, (n) => $isTableCellNode(n));
86 if ($isTableCellNode(node)) {
93 export function $getTableRowNodeFromTableCellNodeOrThrow(
94 startingNode: LexicalNode,
96 const node = $findMatchingParent(startingNode, (n) => $isTableRowNode(n));
98 if ($isTableRowNode(node)) {
102 throw new Error('Expected table cell to be inside of table row.');
105 export function $getTableNodeFromLexicalNodeOrThrow(
106 startingNode: LexicalNode,
108 const node = $findMatchingParent(startingNode, (n) => $isTableNode(n));
110 if ($isTableNode(node)) {
114 throw new Error('Expected table cell to be inside of table.');
117 export function $getTableRowIndexFromTableCellNode(
118 tableCellNode: TableCellNode,
120 const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
121 const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
122 return tableNode.getChildren().findIndex((n) => n.is(tableRowNode));
125 export function $getTableColumnIndexFromTableCellNode(
126 tableCellNode: TableCellNode,
128 const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
129 return tableRowNode.getChildren().findIndex((n) => n.is(tableCellNode));
132 export type TableCellSiblings = {
133 above: TableCellNode | null | undefined;
134 below: TableCellNode | null | undefined;
135 left: TableCellNode | null | undefined;
136 right: TableCellNode | null | undefined;
139 export function $getTableCellSiblingsFromTableCellNode(
140 tableCellNode: TableCellNode,
141 table: TableDOMTable,
142 ): TableCellSiblings {
143 const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
144 const {x, y} = tableNode.getCordsFromCellNode(tableCellNode, table);
146 above: tableNode.getCellNodeFromCords(x, y - 1, table),
147 below: tableNode.getCellNodeFromCords(x, y + 1, table),
148 left: tableNode.getCellNodeFromCords(x - 1, y, table),
149 right: tableNode.getCellNodeFromCords(x + 1, y, table),
153 export function $removeTableRowAtIndex(
154 tableNode: TableNode,
155 indexToDelete: number,
157 const tableRows = tableNode.getChildren();
159 if (indexToDelete >= tableRows.length || indexToDelete < 0) {
160 throw new Error('Expected table cell to be inside of table row.');
163 const targetRowNode = tableRows[indexToDelete];
164 targetRowNode.remove();
168 export function $insertTableRow(
169 tableNode: TableNode,
171 shouldInsertAfter = true,
173 table: TableDOMTable,
175 const tableRows = tableNode.getChildren();
177 if (targetIndex >= tableRows.length || targetIndex < 0) {
178 throw new Error('Table row target index out of range');
181 const targetRowNode = tableRows[targetIndex];
183 if ($isTableRowNode(targetRowNode)) {
184 for (let r = 0; r < rowCount; r++) {
185 const tableRowCells = targetRowNode.getChildren<TableCellNode>();
186 const tableColumnCount = tableRowCells.length;
187 const newTableRowNode = $createTableRowNode();
189 for (let c = 0; c < tableColumnCount; c++) {
190 const tableCellFromTargetRow = tableRowCells[c];
193 $isTableCellNode(tableCellFromTargetRow),
194 'Expected table cell',
197 const {above, below} = $getTableCellSiblingsFromTableCellNode(
198 tableCellFromTargetRow,
202 let headerState = TableCellHeaderStates.NO_STATUS;
204 (above && above.getWidth()) ||
205 (below && below.getWidth()) ||
209 (above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) ||
210 (below && below.hasHeaderState(TableCellHeaderStates.COLUMN))
212 headerState |= TableCellHeaderStates.COLUMN;
215 const tableCellNode = $createTableCellNode(headerState, 1, width);
217 tableCellNode.append($createParagraphNode());
219 newTableRowNode.append(tableCellNode);
222 if (shouldInsertAfter) {
223 targetRowNode.insertAfter(newTableRowNode);
225 targetRowNode.insertBefore(newTableRowNode);
229 throw new Error('Row before insertion index does not exist.');
235 const getHeaderState = (
236 currentState: TableCellHeaderState,
237 possibleState: TableCellHeaderState,
238 ): TableCellHeaderState => {
240 currentState === TableCellHeaderStates.BOTH ||
241 currentState === possibleState
243 return possibleState;
245 return TableCellHeaderStates.NO_STATUS;
248 export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void {
249 const selection = $getSelection();
251 $isRangeSelection(selection) || $isTableSelection(selection),
252 'Expected a RangeSelection or TableSelection',
254 const focus = selection.focus.getNode();
255 const [focusCell, , grid] = $getNodeTriplet(focus);
256 const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell);
257 const columnCount = gridMap[0].length;
258 const {startRow: focusStartRow} = focusCellMap;
260 const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
261 const focusEndRowMap = gridMap[focusEndRow];
262 const newRow = $createTableRowNode();
263 for (let i = 0; i < columnCount; i++) {
264 const {cell, startRow} = focusEndRowMap[i];
265 if (startRow + cell.__rowSpan - 1 <= focusEndRow) {
266 const currentCell = focusEndRowMap[i].cell as TableCellNode;
267 const currentCellHeaderState = currentCell.__headerState;
269 const headerState = getHeaderState(
270 currentCellHeaderState,
271 TableCellHeaderStates.COLUMN,
275 $createTableCellNode(headerState).append($createParagraphNode()),
278 cell.setRowSpan(cell.__rowSpan + 1);
281 const focusEndRowNode = grid.getChildAtIndex(focusEndRow);
283 $isTableRowNode(focusEndRowNode),
284 'focusEndRow is not a TableRowNode',
286 focusEndRowNode.insertAfter(newRow);
288 const focusStartRowMap = gridMap[focusStartRow];
289 const newRow = $createTableRowNode();
290 for (let i = 0; i < columnCount; i++) {
291 const {cell, startRow} = focusStartRowMap[i];
292 if (startRow === focusStartRow) {
293 const currentCell = focusStartRowMap[i].cell as TableCellNode;
294 const currentCellHeaderState = currentCell.__headerState;
296 const headerState = getHeaderState(
297 currentCellHeaderState,
298 TableCellHeaderStates.COLUMN,
302 $createTableCellNode(headerState).append($createParagraphNode()),
305 cell.setRowSpan(cell.__rowSpan + 1);
308 const focusStartRowNode = grid.getChildAtIndex(focusStartRow);
310 $isTableRowNode(focusStartRowNode),
311 'focusEndRow is not a TableRowNode',
313 focusStartRowNode.insertBefore(newRow);
317 export function $insertTableColumn(
318 tableNode: TableNode,
320 shouldInsertAfter = true,
322 table: TableDOMTable,
324 const tableRows = tableNode.getChildren();
326 const tableCellsToBeInserted = [];
327 for (let r = 0; r < tableRows.length; r++) {
328 const currentTableRowNode = tableRows[r];
330 if ($isTableRowNode(currentTableRowNode)) {
331 for (let c = 0; c < columnCount; c++) {
332 const tableRowChildren = currentTableRowNode.getChildren();
333 if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
334 throw new Error('Table column target index out of range');
337 const targetCell = tableRowChildren[targetIndex];
339 invariant($isTableCellNode(targetCell), 'Expected table cell');
341 const {left, right} = $getTableCellSiblingsFromTableCellNode(
346 let headerState = TableCellHeaderStates.NO_STATUS;
349 (left && left.hasHeaderState(TableCellHeaderStates.ROW)) ||
350 (right && right.hasHeaderState(TableCellHeaderStates.ROW))
352 headerState |= TableCellHeaderStates.ROW;
355 const newTableCell = $createTableCellNode(headerState);
357 newTableCell.append($createParagraphNode());
358 tableCellsToBeInserted.push({
365 tableCellsToBeInserted.forEach(({newTableCell, targetCell}) => {
366 if (shouldInsertAfter) {
367 targetCell.insertAfter(newTableCell);
369 targetCell.insertBefore(newTableCell);
376 export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void {
377 const selection = $getSelection();
379 $isRangeSelection(selection) || $isTableSelection(selection),
380 'Expected a RangeSelection or TableSelection',
382 const anchor = selection.anchor.getNode();
383 const focus = selection.focus.getNode();
384 const [anchorCell] = $getNodeTriplet(anchor);
385 const [focusCell, , grid] = $getNodeTriplet(focus);
386 const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(
391 const rowCount = gridMap.length;
392 const startColumn = insertAfter
393 ? Math.max(focusCellMap.startColumn, anchorCellMap.startColumn)
394 : Math.min(focusCellMap.startColumn, anchorCellMap.startColumn);
395 const insertAfterColumn = insertAfter
396 ? startColumn + focusCell.__colSpan - 1
398 const gridFirstChild = grid.getFirstChild();
400 $isTableRowNode(gridFirstChild),
401 'Expected firstTable child to be a row',
403 let firstInsertedCell: null | TableCellNode = null;
404 function $createTableCellNodeForInsertTableColumn(
405 headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
407 const cell = $createTableCellNode(headerState).append(
408 $createParagraphNode(),
410 if (firstInsertedCell === null) {
411 firstInsertedCell = cell;
415 let loopRow: TableRowNode = gridFirstChild;
416 rowLoop: for (let i = 0; i < rowCount; i++) {
418 const currentRow = loopRow.getNextSibling();
420 $isTableRowNode(currentRow),
421 'Expected row nextSibling to be a row',
423 loopRow = currentRow;
425 const rowMap = gridMap[i];
427 const currentCellHeaderState = (
428 rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn]
429 .cell as TableCellNode
432 const headerState = getHeaderState(
433 currentCellHeaderState,
434 TableCellHeaderStates.ROW,
437 if (insertAfterColumn < 0) {
440 $createTableCellNodeForInsertTableColumn(headerState),
446 startColumn: currentStartColumn,
447 startRow: currentStartRow,
448 } = rowMap[insertAfterColumn];
449 if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
450 let insertAfterCell: TableCellNode = currentCell;
451 let insertAfterCellRowStart = currentStartRow;
452 let prevCellIndex = insertAfterColumn;
453 while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
454 prevCellIndex -= currentCell.__colSpan;
455 if (prevCellIndex >= 0) {
456 const {cell: cell_, startRow: startRow_} = rowMap[prevCellIndex];
457 insertAfterCell = cell_;
458 insertAfterCellRowStart = startRow_;
460 loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
464 insertAfterCell.insertAfter(
465 $createTableCellNodeForInsertTableColumn(headerState),
468 currentCell.setColSpan(currentCell.__colSpan + 1);
471 if (firstInsertedCell !== null) {
472 $moveSelectionToCell(firstInsertedCell);
476 export function $deleteTableColumn(
477 tableNode: TableNode,
480 const tableRows = tableNode.getChildren();
482 for (let i = 0; i < tableRows.length; i++) {
483 const currentTableRowNode = tableRows[i];
485 if ($isTableRowNode(currentTableRowNode)) {
486 const tableRowChildren = currentTableRowNode.getChildren();
488 if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
489 throw new Error('Table column target index out of range');
492 tableRowChildren[targetIndex].remove();
499 export function $deleteTableRow__EXPERIMENTAL(): void {
500 const selection = $getSelection();
502 $isRangeSelection(selection) || $isTableSelection(selection),
503 'Expected a RangeSelection or TableSelection',
505 const anchor = selection.anchor.getNode();
506 const focus = selection.focus.getNode();
507 const [anchorCell, , grid] = $getNodeTriplet(anchor);
508 const [focusCell] = $getNodeTriplet(focus);
509 const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
514 const {startRow: anchorStartRow} = anchorCellMap;
515 const {startRow: focusStartRow} = focusCellMap;
516 const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
517 if (gridMap.length === focusEndRow - anchorStartRow + 1) {
522 const columnCount = gridMap[0].length;
523 const nextRow = gridMap[focusEndRow + 1];
524 const nextRowNode: null | TableRowNode = grid.getChildAtIndex(
527 for (let row = focusEndRow; row >= anchorStartRow; row--) {
528 for (let column = columnCount - 1; column >= 0; column--) {
531 startRow: cellStartRow,
532 startColumn: cellStartColumn,
533 } = gridMap[row][column];
534 if (cellStartColumn !== column) {
535 // Don't repeat work for the same Cell
538 // Rows overflowing top have to be trimmed
539 if (row === anchorStartRow && cellStartRow < anchorStartRow) {
540 cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow));
542 // Rows overflowing bottom have to be trimmed and moved to the next row
544 cellStartRow >= anchorStartRow &&
545 cellStartRow + cell.__rowSpan - 1 > focusEndRow
547 cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1));
548 invariant(nextRowNode !== null, 'Expected nextRowNode not to be null');
550 $insertFirst(nextRowNode, cell);
552 const {cell: previousCell} = nextRow[column - 1];
553 previousCell.insertAfter(cell);
557 const rowNode = grid.getChildAtIndex(row);
559 $isTableRowNode(rowNode),
560 'Expected GridNode childAtIndex(%s) to be RowNode',
565 if (nextRow !== undefined) {
566 const {cell} = nextRow[0];
567 $moveSelectionToCell(cell);
569 const previousRow = gridMap[anchorStartRow - 1];
570 const {cell} = previousRow[0];
571 $moveSelectionToCell(cell);
575 export function $deleteTableColumn__EXPERIMENTAL(): void {
576 const selection = $getSelection();
578 $isRangeSelection(selection) || $isTableSelection(selection),
579 'Expected a RangeSelection or TableSelection',
581 const anchor = selection.anchor.getNode();
582 const focus = selection.focus.getNode();
583 const [anchorCell, , grid] = $getNodeTriplet(anchor);
584 const [focusCell] = $getNodeTriplet(focus);
585 const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
590 const {startColumn: anchorStartColumn} = anchorCellMap;
591 const {startRow: focusStartRow, startColumn: focusStartColumn} = focusCellMap;
592 const startColumn = Math.min(anchorStartColumn, focusStartColumn);
593 const endColumn = Math.max(
594 anchorStartColumn + anchorCell.__colSpan - 1,
595 focusStartColumn + focusCell.__colSpan - 1,
597 const selectedColumnCount = endColumn - startColumn + 1;
598 const columnCount = gridMap[0].length;
599 if (columnCount === endColumn - startColumn + 1) {
601 grid.selectPrevious();
605 const rowCount = gridMap.length;
606 for (let row = 0; row < rowCount; row++) {
607 for (let column = startColumn; column <= endColumn; column++) {
608 const {cell, startColumn: cellStartColumn} = gridMap[row][column];
609 if (cellStartColumn < startColumn) {
610 if (column === startColumn) {
611 const overflowLeft = startColumn - cellStartColumn;
615 // Possible overflow right too
616 Math.min(selectedColumnCount, cell.__colSpan - overflowLeft),
619 } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
620 if (column === endColumn) {
622 const inSelectedArea = endColumn - cellStartColumn + 1;
623 cell.setColSpan(cell.__colSpan - inSelectedArea);
630 const focusRowMap = gridMap[focusStartRow];
632 anchorStartColumn > focusStartColumn
633 ? focusRowMap[anchorStartColumn + anchorCell.__colSpan]
634 : focusRowMap[focusStartColumn + focusCell.__colSpan];
635 if (nextColumn !== undefined) {
636 const {cell} = nextColumn;
637 $moveSelectionToCell(cell);
640 focusStartColumn < anchorStartColumn
641 ? focusRowMap[focusStartColumn - 1]
642 : focusRowMap[anchorStartColumn - 1];
643 const {cell} = previousRow;
644 $moveSelectionToCell(cell);
648 function $moveSelectionToCell(cell: TableCellNode): void {
649 const firstDescendant = cell.getFirstDescendant();
650 if (firstDescendant == null) {
653 firstDescendant.getParentOrThrow().selectStart();
657 function $insertFirst(parent: ElementNode, node: LexicalNode): void {
658 const firstChild = parent.getFirstChild();
659 if (firstChild !== null) {
660 firstChild.insertBefore(node);
666 export function $unmergeCell(): void {
667 const selection = $getSelection();
669 $isRangeSelection(selection) || $isTableSelection(selection),
670 'Expected a RangeSelection or TableSelection',
672 const anchor = selection.anchor.getNode();
673 const [cell, row, grid] = $getNodeTriplet(anchor);
674 const colSpan = cell.__colSpan;
675 const rowSpan = cell.__rowSpan;
677 for (let i = 1; i < colSpan; i++) {
679 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
680 $createParagraphNode(),
687 const [map, cellMap] = $computeTableMap(grid, cell, cell);
688 const {startColumn, startRow} = cellMap;
690 for (let i = 1; i < rowSpan; i++) {
691 const currentRow = startRow + i;
692 const currentRowMap = map[currentRow];
693 currentRowNode = (currentRowNode || row).getNextSibling();
695 $isTableRowNode(currentRowNode),
696 'Expected row next sibling to be a row',
698 let insertAfterCell: null | TableCellNode = null;
699 for (let column = 0; column < startColumn; column++) {
700 const currentCellMap = currentRowMap[column];
701 const currentCell = currentCellMap.cell;
702 if (currentCellMap.startRow === currentRow) {
703 insertAfterCell = currentCell;
705 if (currentCell.__colSpan > 1) {
706 column += currentCell.__colSpan - 1;
709 if (insertAfterCell === null) {
710 for (let j = 0; j < colSpan; j++) {
713 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
714 $createParagraphNode(),
719 for (let j = 0; j < colSpan; j++) {
720 insertAfterCell.insertAfter(
721 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
722 $createParagraphNode(),
732 export function $computeTableMap(
734 cellA: TableCellNode,
735 cellB: TableCellNode,
736 ): [TableMapType, TableMapValueType, TableMapValueType] {
737 const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(
742 invariant(cellAValue !== null, 'Anchor not found in Grid');
743 invariant(cellBValue !== null, 'Focus not found in Grid');
744 return [tableMap, cellAValue, cellBValue];
747 export function $computeTableMapSkipCellCheck(
749 cellA: null | TableCellNode,
750 cellB: null | TableCellNode,
751 ): [TableMapType, TableMapValueType | null, TableMapValueType | null] {
752 const tableMap: TableMapType = [];
753 let cellAValue: null | TableMapValueType = null;
754 let cellBValue: null | TableMapValueType = null;
755 function write(startRow: number, startColumn: number, cell: TableCellNode) {
761 const rowSpan = cell.__rowSpan;
762 const colSpan = cell.__colSpan;
763 for (let i = 0; i < rowSpan; i++) {
764 if (tableMap[startRow + i] === undefined) {
765 tableMap[startRow + i] = [];
767 for (let j = 0; j < colSpan; j++) {
768 tableMap[startRow + i][startColumn + j] = value;
771 if (cellA !== null && cellA.is(cell)) {
774 if (cellB !== null && cellB.is(cell)) {
778 function isEmpty(row: number, column: number) {
779 return tableMap[row] === undefined || tableMap[row][column] === undefined;
782 const gridChildren = grid.getChildren();
783 for (let i = 0; i < gridChildren.length; i++) {
784 const row = gridChildren[i];
786 $isTableRowNode(row),
787 'Expected GridNode children to be TableRowNode',
789 const rowChildren = row.getChildren();
791 for (const cell of rowChildren) {
793 $isTableCellNode(cell),
794 'Expected TableRowNode children to be TableCellNode',
796 while (!isEmpty(i, j)) {
803 return [tableMap, cellAValue, cellBValue];
806 export function $getNodeTriplet(
807 source: PointType | LexicalNode | TableCellNode,
808 ): [TableCellNode, TableRowNode, TableNode] {
809 let cell: TableCellNode;
810 if (source instanceof TableCellNode) {
812 } else if ('__type' in source) {
813 const cell_ = $findMatchingParent(source, $isTableCellNode);
815 $isTableCellNode(cell_),
816 'Expected to find a parent TableCellNode',
820 const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);
822 $isTableCellNode(cell_),
823 'Expected to find a parent TableCellNode',
827 const row = cell.getParent();
829 $isTableRowNode(row),
830 'Expected TableCellNode to have a parent TableRowNode',
832 const grid = row.getParent();
835 'Expected TableRowNode to have a parent GridNode',
837 return [cell, row, grid];
840 export function $getTableCellNodeRect(tableCellNode: TableCellNode): {
846 const [cellNode, , gridNode] = $getNodeTriplet(tableCellNode);
847 const rows = gridNode.getChildren<TableRowNode>();
848 const rowCount = rows.length;
849 const columnCount = rows[0].getChildren().length;
851 // Create a matrix of the same size as the table to track the position of each cell
852 const cellMatrix = new Array(rowCount);
853 for (let i = 0; i < rowCount; i++) {
854 cellMatrix[i] = new Array(columnCount);
857 for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
858 const row = rows[rowIndex];
859 const cells = row.getChildren<TableCellNode>();
862 for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
863 // Find the next available position in the matrix, skip the position of merged cells
864 while (cellMatrix[rowIndex][columnIndex]) {
868 const cell = cells[cellIndex];
869 const rowSpan = cell.__rowSpan || 1;
870 const colSpan = cell.__colSpan || 1;
872 // Put the cell into the corresponding position in the matrix
873 for (let i = 0; i < rowSpan; i++) {
874 for (let j = 0; j < colSpan; j++) {
875 cellMatrix[rowIndex + i][columnIndex + j] = cell;
879 // Return to the original index, row span and column span of the cell.
880 if (cellNode === cell) {
889 columnIndex += colSpan;