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';
38 import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
40 export function $createTableNodeWithDimensions(
43 includeHeaders: InsertTableCommandPayloadHeaders = true,
45 const tableNode = $createTableNode();
47 for (let iRow = 0; iRow < rowCount; iRow++) {
48 const tableRowNode = $createTableRowNode();
50 for (let iColumn = 0; iColumn < columnCount; iColumn++) {
51 let headerState = TableCellHeaderStates.NO_STATUS;
53 if (typeof includeHeaders === 'object') {
54 if (iRow === 0 && includeHeaders.rows) {
55 headerState |= TableCellHeaderStates.ROW;
57 if (iColumn === 0 && includeHeaders.columns) {
58 headerState |= TableCellHeaderStates.COLUMN;
60 } else if (includeHeaders) {
62 headerState |= TableCellHeaderStates.ROW;
65 headerState |= TableCellHeaderStates.COLUMN;
69 const tableCellNode = $createTableCellNode(headerState);
70 const paragraphNode = $createParagraphNode();
71 paragraphNode.append($createTextNode());
72 tableCellNode.append(paragraphNode);
73 tableRowNode.append(tableCellNode);
76 tableNode.append(tableRowNode);
82 export function $getTableCellNodeFromLexicalNode(
83 startingNode: LexicalNode,
84 ): TableCellNode | null {
85 const node = $findMatchingParent(startingNode, (n) => $isTableCellNode(n));
87 if ($isTableCellNode(node)) {
94 export function $getTableRowNodeFromTableCellNodeOrThrow(
95 startingNode: LexicalNode,
97 const node = $findMatchingParent(startingNode, (n) => $isTableRowNode(n));
99 if ($isTableRowNode(node)) {
103 throw new Error('Expected table cell to be inside of table row.');
106 export function $getTableNodeFromLexicalNodeOrThrow(
107 startingNode: LexicalNode,
109 const node = $findMatchingParent(startingNode, (n) => $isTableNode(n));
111 if ($isTableNode(node)) {
115 throw new Error('Expected table cell to be inside of table.');
118 export function $getTableRowIndexFromTableCellNode(
119 tableCellNode: TableCellNode,
121 const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
122 const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
123 return tableNode.getChildren().findIndex((n) => n.is(tableRowNode));
126 export function $getTableColumnIndexFromTableCellNode(
127 tableCellNode: TableCellNode,
129 const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
130 return tableRowNode.getChildren().findIndex((n) => n.is(tableCellNode));
133 export type TableCellSiblings = {
134 above: TableCellNode | null | undefined;
135 below: TableCellNode | null | undefined;
136 left: TableCellNode | null | undefined;
137 right: TableCellNode | null | undefined;
140 export function $getTableCellSiblingsFromTableCellNode(
141 tableCellNode: TableCellNode,
142 table: TableDOMTable,
143 ): TableCellSiblings {
144 const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
145 const {x, y} = tableNode.getCordsFromCellNode(tableCellNode, table);
147 above: tableNode.getCellNodeFromCords(x, y - 1, table),
148 below: tableNode.getCellNodeFromCords(x, y + 1, table),
149 left: tableNode.getCellNodeFromCords(x - 1, y, table),
150 right: tableNode.getCellNodeFromCords(x + 1, y, table),
154 export function $removeTableRowAtIndex(
155 tableNode: TableNode,
156 indexToDelete: number,
158 const tableRows = tableNode.getChildren();
160 if (indexToDelete >= tableRows.length || indexToDelete < 0) {
161 throw new Error('Expected table cell to be inside of table row.');
164 const targetRowNode = tableRows[indexToDelete];
165 targetRowNode.remove();
169 export function $insertTableRow(
170 tableNode: TableNode,
172 shouldInsertAfter = true,
174 table: TableDOMTable,
176 const tableRows = tableNode.getChildren();
178 if (targetIndex >= tableRows.length || targetIndex < 0) {
179 throw new Error('Table row target index out of range');
182 const targetRowNode = tableRows[targetIndex];
184 if ($isTableRowNode(targetRowNode)) {
185 for (let r = 0; r < rowCount; r++) {
186 const tableRowCells = targetRowNode.getChildren<TableCellNode>();
187 const tableColumnCount = tableRowCells.length;
188 const newTableRowNode = $createTableRowNode();
190 for (let c = 0; c < tableColumnCount; c++) {
191 const tableCellFromTargetRow = tableRowCells[c];
194 $isTableCellNode(tableCellFromTargetRow),
195 'Expected table cell',
198 const {above, below} = $getTableCellSiblingsFromTableCellNode(
199 tableCellFromTargetRow,
203 let headerState = TableCellHeaderStates.NO_STATUS;
205 (above && above.getWidth()) ||
206 (below && below.getWidth()) ||
210 (above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) ||
211 (below && below.hasHeaderState(TableCellHeaderStates.COLUMN))
213 headerState |= TableCellHeaderStates.COLUMN;
216 const tableCellNode = $createTableCellNode(headerState, 1, width);
218 tableCellNode.append($createParagraphNode());
220 newTableRowNode.append(tableCellNode);
223 if (shouldInsertAfter) {
224 targetRowNode.insertAfter(newTableRowNode);
226 targetRowNode.insertBefore(newTableRowNode);
230 throw new Error('Row before insertion index does not exist.');
236 const getHeaderState = (
237 currentState: TableCellHeaderState,
238 possibleState: TableCellHeaderState,
239 ): TableCellHeaderState => {
241 currentState === TableCellHeaderStates.BOTH ||
242 currentState === possibleState
244 return possibleState;
246 return TableCellHeaderStates.NO_STATUS;
249 export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void {
250 const selection = $getSelection();
252 $isRangeSelection(selection) || $isTableSelection(selection),
253 'Expected a RangeSelection or TableSelection',
255 const focus = selection.focus.getNode();
256 const [focusCell, , grid] = $getNodeTriplet(focus);
257 const [gridMap, focusCellMap] = $computeTableMap(grid, focusCell, focusCell);
258 const columnCount = gridMap[0].length;
259 const {startRow: focusStartRow} = focusCellMap;
261 const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
262 const focusEndRowMap = gridMap[focusEndRow];
263 const newRow = $createTableRowNode();
264 for (let i = 0; i < columnCount; i++) {
265 const {cell, startRow} = focusEndRowMap[i];
266 if (startRow + cell.__rowSpan - 1 <= focusEndRow) {
267 const currentCell = focusEndRowMap[i].cell as TableCellNode;
268 const currentCellHeaderState = currentCell.__headerState;
270 const headerState = getHeaderState(
271 currentCellHeaderState,
272 TableCellHeaderStates.COLUMN,
276 $createTableCellNode(headerState).append($createParagraphNode()),
279 cell.setRowSpan(cell.__rowSpan + 1);
282 const focusEndRowNode = grid.getChildAtIndex(focusEndRow);
284 $isTableRowNode(focusEndRowNode),
285 'focusEndRow is not a TableRowNode',
287 focusEndRowNode.insertAfter(newRow);
289 const focusStartRowMap = gridMap[focusStartRow];
290 const newRow = $createTableRowNode();
291 for (let i = 0; i < columnCount; i++) {
292 const {cell, startRow} = focusStartRowMap[i];
293 if (startRow === focusStartRow) {
294 const currentCell = focusStartRowMap[i].cell as TableCellNode;
295 const currentCellHeaderState = currentCell.__headerState;
297 const headerState = getHeaderState(
298 currentCellHeaderState,
299 TableCellHeaderStates.COLUMN,
303 $createTableCellNode(headerState).append($createParagraphNode()),
306 cell.setRowSpan(cell.__rowSpan + 1);
309 const focusStartRowNode = grid.getChildAtIndex(focusStartRow);
311 $isTableRowNode(focusStartRowNode),
312 'focusEndRow is not a TableRowNode',
314 focusStartRowNode.insertBefore(newRow);
318 export function $insertTableColumn(
319 tableNode: TableNode,
321 shouldInsertAfter = true,
323 table: TableDOMTable,
325 const tableRows = tableNode.getChildren();
327 const tableCellsToBeInserted = [];
328 for (let r = 0; r < tableRows.length; r++) {
329 const currentTableRowNode = tableRows[r];
331 if ($isTableRowNode(currentTableRowNode)) {
332 for (let c = 0; c < columnCount; c++) {
333 const tableRowChildren = currentTableRowNode.getChildren();
334 if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
335 throw new Error('Table column target index out of range');
338 const targetCell = tableRowChildren[targetIndex];
340 invariant($isTableCellNode(targetCell), 'Expected table cell');
342 const {left, right} = $getTableCellSiblingsFromTableCellNode(
347 let headerState = TableCellHeaderStates.NO_STATUS;
350 (left && left.hasHeaderState(TableCellHeaderStates.ROW)) ||
351 (right && right.hasHeaderState(TableCellHeaderStates.ROW))
353 headerState |= TableCellHeaderStates.ROW;
356 const newTableCell = $createTableCellNode(headerState);
358 newTableCell.append($createParagraphNode());
359 tableCellsToBeInserted.push({
366 tableCellsToBeInserted.forEach(({newTableCell, targetCell}) => {
367 if (shouldInsertAfter) {
368 targetCell.insertAfter(newTableCell);
370 targetCell.insertBefore(newTableCell);
377 export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void {
378 const selection = $getSelection();
380 $isRangeSelection(selection) || $isTableSelection(selection),
381 'Expected a RangeSelection or TableSelection',
383 const anchor = selection.anchor.getNode();
384 const focus = selection.focus.getNode();
385 const [anchorCell] = $getNodeTriplet(anchor);
386 const [focusCell, , grid] = $getNodeTriplet(focus);
387 const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(
392 const rowCount = gridMap.length;
393 const startColumn = insertAfter
394 ? Math.max(focusCellMap.startColumn, anchorCellMap.startColumn)
395 : Math.min(focusCellMap.startColumn, anchorCellMap.startColumn);
396 const insertAfterColumn = insertAfter
397 ? startColumn + focusCell.__colSpan - 1
399 const gridFirstChild = grid.getFirstChild();
401 $isTableRowNode(gridFirstChild),
402 'Expected firstTable child to be a row',
404 let firstInsertedCell: null | TableCellNode = null;
405 function $createTableCellNodeForInsertTableColumn(
406 headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
408 const cell = $createTableCellNode(headerState).append(
409 $createParagraphNode(),
411 if (firstInsertedCell === null) {
412 firstInsertedCell = cell;
416 let loopRow: TableRowNode = gridFirstChild;
417 rowLoop: for (let i = 0; i < rowCount; i++) {
419 const currentRow = loopRow.getNextSibling();
421 $isTableRowNode(currentRow),
422 'Expected row nextSibling to be a row',
424 loopRow = currentRow;
426 const rowMap = gridMap[i];
428 const currentCellHeaderState = (
429 rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn]
430 .cell as TableCellNode
433 const headerState = getHeaderState(
434 currentCellHeaderState,
435 TableCellHeaderStates.ROW,
438 if (insertAfterColumn < 0) {
441 $createTableCellNodeForInsertTableColumn(headerState),
447 startColumn: currentStartColumn,
448 startRow: currentStartRow,
449 } = rowMap[insertAfterColumn];
450 if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
451 let insertAfterCell: TableCellNode = currentCell;
452 let insertAfterCellRowStart = currentStartRow;
453 let prevCellIndex = insertAfterColumn;
454 while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
455 prevCellIndex -= currentCell.__colSpan;
456 if (prevCellIndex >= 0) {
457 const {cell: cell_, startRow: startRow_} = rowMap[prevCellIndex];
458 insertAfterCell = cell_;
459 insertAfterCellRowStart = startRow_;
461 loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
465 insertAfterCell.insertAfter(
466 $createTableCellNodeForInsertTableColumn(headerState),
469 currentCell.setColSpan(currentCell.__colSpan + 1);
472 if (firstInsertedCell !== null) {
473 $moveSelectionToCell(firstInsertedCell);
477 export function $deleteTableColumn(
478 tableNode: TableNode,
481 const tableRows = tableNode.getChildren();
483 for (let i = 0; i < tableRows.length; i++) {
484 const currentTableRowNode = tableRows[i];
486 if ($isTableRowNode(currentTableRowNode)) {
487 const tableRowChildren = currentTableRowNode.getChildren();
489 if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
490 throw new Error('Table column target index out of range');
493 tableRowChildren[targetIndex].remove();
500 export function $deleteTableRow__EXPERIMENTAL(): void {
501 const selection = $getSelection();
503 $isRangeSelection(selection) || $isTableSelection(selection),
504 'Expected a RangeSelection or TableSelection',
506 const anchor = selection.anchor.getNode();
507 const focus = selection.focus.getNode();
508 const [anchorCell, , grid] = $getNodeTriplet(anchor);
509 const [focusCell] = $getNodeTriplet(focus);
510 const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
515 const {startRow: anchorStartRow} = anchorCellMap;
516 const {startRow: focusStartRow} = focusCellMap;
517 const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
518 if (gridMap.length === focusEndRow - anchorStartRow + 1) {
523 const columnCount = gridMap[0].length;
524 const nextRow = gridMap[focusEndRow + 1];
525 const nextRowNode: null | TableRowNode = grid.getChildAtIndex(
528 for (let row = focusEndRow; row >= anchorStartRow; row--) {
529 for (let column = columnCount - 1; column >= 0; column--) {
532 startRow: cellStartRow,
533 startColumn: cellStartColumn,
534 } = gridMap[row][column];
535 if (cellStartColumn !== column) {
536 // Don't repeat work for the same Cell
539 // Rows overflowing top have to be trimmed
540 if (row === anchorStartRow && cellStartRow < anchorStartRow) {
541 cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow));
543 // Rows overflowing bottom have to be trimmed and moved to the next row
545 cellStartRow >= anchorStartRow &&
546 cellStartRow + cell.__rowSpan - 1 > focusEndRow
548 cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1));
549 invariant(nextRowNode !== null, 'Expected nextRowNode not to be null');
551 $insertFirst(nextRowNode, cell);
553 const {cell: previousCell} = nextRow[column - 1];
554 previousCell.insertAfter(cell);
558 const rowNode = grid.getChildAtIndex(row);
560 $isTableRowNode(rowNode),
561 'Expected GridNode childAtIndex(%s) to be RowNode',
566 if (nextRow !== undefined) {
567 const {cell} = nextRow[0];
568 $moveSelectionToCell(cell);
570 const previousRow = gridMap[anchorStartRow - 1];
571 const {cell} = previousRow[0];
572 $moveSelectionToCell(cell);
576 export function $deleteTableColumn__EXPERIMENTAL(): void {
577 const selection = $getSelection();
579 $isRangeSelection(selection) || $isTableSelection(selection),
580 'Expected a RangeSelection or TableSelection',
582 const anchor = selection.anchor.getNode();
583 const focus = selection.focus.getNode();
584 const [anchorCell, , grid] = $getNodeTriplet(anchor);
585 const [focusCell] = $getNodeTriplet(focus);
586 const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(
591 const {startColumn: anchorStartColumn} = anchorCellMap;
592 const {startRow: focusStartRow, startColumn: focusStartColumn} = focusCellMap;
593 const startColumn = Math.min(anchorStartColumn, focusStartColumn);
594 const endColumn = Math.max(
595 anchorStartColumn + anchorCell.__colSpan - 1,
596 focusStartColumn + focusCell.__colSpan - 1,
598 const selectedColumnCount = endColumn - startColumn + 1;
599 const columnCount = gridMap[0].length;
600 if (columnCount === endColumn - startColumn + 1) {
602 grid.selectPrevious();
606 const rowCount = gridMap.length;
607 for (let row = 0; row < rowCount; row++) {
608 for (let column = startColumn; column <= endColumn; column++) {
609 const {cell, startColumn: cellStartColumn} = gridMap[row][column];
610 if (cellStartColumn < startColumn) {
611 if (column === startColumn) {
612 const overflowLeft = startColumn - cellStartColumn;
616 // Possible overflow right too
617 Math.min(selectedColumnCount, cell.__colSpan - overflowLeft),
620 } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
621 if (column === endColumn) {
623 const inSelectedArea = endColumn - cellStartColumn + 1;
624 cell.setColSpan(cell.__colSpan - inSelectedArea);
631 const focusRowMap = gridMap[focusStartRow];
633 anchorStartColumn > focusStartColumn
634 ? focusRowMap[anchorStartColumn + anchorCell.__colSpan]
635 : focusRowMap[focusStartColumn + focusCell.__colSpan];
636 if (nextColumn !== undefined) {
637 const {cell} = nextColumn;
638 $moveSelectionToCell(cell);
641 focusStartColumn < anchorStartColumn
642 ? focusRowMap[focusStartColumn - 1]
643 : focusRowMap[anchorStartColumn - 1];
644 const {cell} = previousRow;
645 $moveSelectionToCell(cell);
649 function $moveSelectionToCell(cell: TableCellNode): void {
650 const firstDescendant = cell.getFirstDescendant();
651 if (firstDescendant == null) {
654 firstDescendant.getParentOrThrow().selectStart();
658 function $insertFirst(parent: ElementNode, node: LexicalNode): void {
659 const firstChild = parent.getFirstChild();
660 if (firstChild !== null) {
661 firstChild.insertBefore(node);
667 export function $unmergeCell(): void {
668 const selection = $getSelection();
670 $isRangeSelection(selection) || $isTableSelection(selection),
671 'Expected a RangeSelection or TableSelection',
673 const anchor = selection.anchor.getNode();
674 const [cell, row, grid] = $getNodeTriplet(anchor);
675 const colSpan = cell.__colSpan;
676 const rowSpan = cell.__rowSpan;
678 for (let i = 1; i < colSpan; i++) {
680 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
681 $createParagraphNode(),
688 const [map, cellMap] = $computeTableMap(grid, cell, cell);
689 const {startColumn, startRow} = cellMap;
691 for (let i = 1; i < rowSpan; i++) {
692 const currentRow = startRow + i;
693 const currentRowMap = map[currentRow];
694 currentRowNode = (currentRowNode || row).getNextSibling();
696 $isTableRowNode(currentRowNode),
697 'Expected row next sibling to be a row',
699 let insertAfterCell: null | TableCellNode = null;
700 for (let column = 0; column < startColumn; column++) {
701 const currentCellMap = currentRowMap[column];
702 const currentCell = currentCellMap.cell;
703 if (currentCellMap.startRow === currentRow) {
704 insertAfterCell = currentCell;
706 if (currentCell.__colSpan > 1) {
707 column += currentCell.__colSpan - 1;
710 if (insertAfterCell === null) {
711 for (let j = 0; j < colSpan; j++) {
714 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
715 $createParagraphNode(),
720 for (let j = 0; j < colSpan; j++) {
721 insertAfterCell.insertAfter(
722 $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
723 $createParagraphNode(),
733 export function $computeTableMap(
735 cellA: TableCellNode,
736 cellB: TableCellNode,
737 ): [TableMapType, TableMapValueType, TableMapValueType] {
738 const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(
743 invariant(cellAValue !== null, 'Anchor not found in Grid');
744 invariant(cellBValue !== null, 'Focus not found in Grid');
745 return [tableMap, cellAValue, cellBValue];
748 export function $computeTableMapSkipCellCheck(
750 cellA: null | TableCellNode,
751 cellB: null | TableCellNode,
752 ): [TableMapType, TableMapValueType | null, TableMapValueType | null] {
753 const tableMap: TableMapType = [];
754 let cellAValue: null | TableMapValueType = null;
755 let cellBValue: null | TableMapValueType = null;
756 function write(startRow: number, startColumn: number, cell: TableCellNode) {
762 const rowSpan = cell.__rowSpan;
763 const colSpan = cell.__colSpan;
764 for (let i = 0; i < rowSpan; i++) {
765 if (tableMap[startRow + i] === undefined) {
766 tableMap[startRow + i] = [];
768 for (let j = 0; j < colSpan; j++) {
769 tableMap[startRow + i][startColumn + j] = value;
772 if (cellA !== null && cellA.is(cell)) {
775 if (cellB !== null && cellB.is(cell)) {
779 function isEmpty(row: number, column: number) {
780 return tableMap[row] === undefined || tableMap[row][column] === undefined;
783 const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node));
784 for (let i = 0; i < gridChildren.length; i++) {
785 const row = gridChildren[i];
787 $isTableRowNode(row),
788 'Expected GridNode children to be TableRowNode',
790 const rowChildren = row.getChildren();
792 for (const cell of rowChildren) {
794 $isTableCellNode(cell),
795 'Expected TableRowNode children to be TableCellNode',
797 while (!isEmpty(i, j)) {
804 return [tableMap, cellAValue, cellBValue];
807 export function $getNodeTriplet(
808 source: PointType | LexicalNode | TableCellNode,
809 ): [TableCellNode, TableRowNode, TableNode] {
810 let cell: TableCellNode;
811 if (source instanceof TableCellNode) {
813 } else if ('__type' in source) {
814 const cell_ = $findMatchingParent(source, $isTableCellNode);
816 $isTableCellNode(cell_),
817 'Expected to find a parent TableCellNode',
821 const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);
823 $isTableCellNode(cell_),
824 'Expected to find a parent TableCellNode',
828 const row = cell.getParent();
830 $isTableRowNode(row),
831 'Expected TableCellNode to have a parent TableRowNode',
833 const grid = row.getParent();
836 'Expected TableRowNode to have a parent GridNode',
838 return [cell, row, grid];
841 export function $getTableCellNodeRect(tableCellNode: TableCellNode): {
847 const [cellNode, , gridNode] = $getNodeTriplet(tableCellNode);
848 const rows = gridNode.getChildren<TableRowNode>();
849 const rowCount = rows.length;
850 const columnCount = rows[0].getChildren().length;
852 // Create a matrix of the same size as the table to track the position of each cell
853 const cellMatrix = new Array(rowCount);
854 for (let i = 0; i < rowCount; i++) {
855 cellMatrix[i] = new Array(columnCount);
858 for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
859 const row = rows[rowIndex];
860 const cells = row.getChildren<TableCellNode>();
863 for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
864 // Find the next available position in the matrix, skip the position of merged cells
865 while (cellMatrix[rowIndex][columnIndex]) {
869 const cell = cells[cellIndex];
870 const rowSpan = cell.__rowSpan || 1;
871 const colSpan = cell.__colSpan || 1;
873 // Put the cell into the corresponding position in the matrix
874 for (let i = 0; i < rowSpan; i++) {
875 for (let j = 0; j < colSpan; j++) {
876 cellMatrix[rowIndex + i][columnIndex + j] = cell;
880 // Return to the original index, row span and column span of the cell.
881 if (cellNode === cell) {
890 columnIndex += colSpan;