]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / table / LexicalTableUtils.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {TableMapType, TableMapValueType} from './LexicalTableSelection';
10 import type {ElementNode, PointType} from 'lexical';
11
12 import {$findMatchingParent} from '@lexical/utils';
13 import {
14   $createParagraphNode,
15   $createTextNode,
16   $getSelection,
17   $isRangeSelection,
18   LexicalNode,
19 } from 'lexical';
20 import invariant from 'lexical/shared/invariant';
21
22 import {InsertTableCommandPayloadHeaders} from '.';
23 import {
24   $createTableCellNode,
25   $isTableCellNode,
26   TableCellHeaderState,
27   TableCellHeaderStates,
28   TableCellNode,
29 } from './LexicalTableCellNode';
30 import {$createTableNode, $isTableNode, TableNode} from './LexicalTableNode';
31 import {TableDOMTable} from './LexicalTableObserver';
32 import {
33   $createTableRowNode,
34   $isTableRowNode,
35   TableRowNode,
36 } from './LexicalTableRowNode';
37 import {$isTableSelection} from './LexicalTableSelection';
38 import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
39
40 export function $createTableNodeWithDimensions(
41   rowCount: number,
42   columnCount: number,
43   includeHeaders: InsertTableCommandPayloadHeaders = true,
44 ): TableNode {
45   const tableNode = $createTableNode();
46
47   for (let iRow = 0; iRow < rowCount; iRow++) {
48     const tableRowNode = $createTableRowNode();
49
50     for (let iColumn = 0; iColumn < columnCount; iColumn++) {
51       let headerState = TableCellHeaderStates.NO_STATUS;
52
53       if (typeof includeHeaders === 'object') {
54         if (iRow === 0 && includeHeaders.rows) {
55           headerState |= TableCellHeaderStates.ROW;
56         }
57         if (iColumn === 0 && includeHeaders.columns) {
58           headerState |= TableCellHeaderStates.COLUMN;
59         }
60       } else if (includeHeaders) {
61         if (iRow === 0) {
62           headerState |= TableCellHeaderStates.ROW;
63         }
64         if (iColumn === 0) {
65           headerState |= TableCellHeaderStates.COLUMN;
66         }
67       }
68
69       const tableCellNode = $createTableCellNode(headerState);
70       const paragraphNode = $createParagraphNode();
71       paragraphNode.append($createTextNode());
72       tableCellNode.append(paragraphNode);
73       tableRowNode.append(tableCellNode);
74     }
75
76     tableNode.append(tableRowNode);
77   }
78
79   return tableNode;
80 }
81
82 export function $getTableCellNodeFromLexicalNode(
83   startingNode: LexicalNode,
84 ): TableCellNode | null {
85   const node = $findMatchingParent(startingNode, (n) => $isTableCellNode(n));
86
87   if ($isTableCellNode(node)) {
88     return node;
89   }
90
91   return null;
92 }
93
94 export function $getTableRowNodeFromTableCellNodeOrThrow(
95   startingNode: LexicalNode,
96 ): TableRowNode {
97   const node = $findMatchingParent(startingNode, (n) => $isTableRowNode(n));
98
99   if ($isTableRowNode(node)) {
100     return node;
101   }
102
103   throw new Error('Expected table cell to be inside of table row.');
104 }
105
106 export function $getTableNodeFromLexicalNodeOrThrow(
107   startingNode: LexicalNode,
108 ): TableNode {
109   const node = $findMatchingParent(startingNode, (n) => $isTableNode(n));
110
111   if ($isTableNode(node)) {
112     return node;
113   }
114
115   throw new Error('Expected table cell to be inside of table.');
116 }
117
118 export function $getTableRowIndexFromTableCellNode(
119   tableCellNode: TableCellNode,
120 ): number {
121   const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
122   const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
123   return tableNode.getChildren().findIndex((n) => n.is(tableRowNode));
124 }
125
126 export function $getTableColumnIndexFromTableCellNode(
127   tableCellNode: TableCellNode,
128 ): number {
129   const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
130   return tableRowNode.getChildren().findIndex((n) => n.is(tableCellNode));
131 }
132
133 export type TableCellSiblings = {
134   above: TableCellNode | null | undefined;
135   below: TableCellNode | null | undefined;
136   left: TableCellNode | null | undefined;
137   right: TableCellNode | null | undefined;
138 };
139
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);
146   return {
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),
151   };
152 }
153
154 export function $removeTableRowAtIndex(
155   tableNode: TableNode,
156   indexToDelete: number,
157 ): TableNode {
158   const tableRows = tableNode.getChildren();
159
160   if (indexToDelete >= tableRows.length || indexToDelete < 0) {
161     throw new Error('Expected table cell to be inside of table row.');
162   }
163
164   const targetRowNode = tableRows[indexToDelete];
165   targetRowNode.remove();
166   return tableNode;
167 }
168
169 export function $insertTableRow(
170   tableNode: TableNode,
171   targetIndex: number,
172   shouldInsertAfter = true,
173   rowCount: number,
174   table: TableDOMTable,
175 ): TableNode {
176   const tableRows = tableNode.getChildren();
177
178   if (targetIndex >= tableRows.length || targetIndex < 0) {
179     throw new Error('Table row target index out of range');
180   }
181
182   const targetRowNode = tableRows[targetIndex];
183
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();
189
190       for (let c = 0; c < tableColumnCount; c++) {
191         const tableCellFromTargetRow = tableRowCells[c];
192
193         invariant(
194           $isTableCellNode(tableCellFromTargetRow),
195           'Expected table cell',
196         );
197
198         const {above, below} = $getTableCellSiblingsFromTableCellNode(
199           tableCellFromTargetRow,
200           table,
201         );
202
203         let headerState = TableCellHeaderStates.NO_STATUS;
204         const width =
205           (above && above.getWidth()) ||
206           (below && below.getWidth()) ||
207           undefined;
208
209         if (
210           (above && above.hasHeaderState(TableCellHeaderStates.COLUMN)) ||
211           (below && below.hasHeaderState(TableCellHeaderStates.COLUMN))
212         ) {
213           headerState |= TableCellHeaderStates.COLUMN;
214         }
215
216         const tableCellNode = $createTableCellNode(headerState, 1, width);
217
218         tableCellNode.append($createParagraphNode());
219
220         newTableRowNode.append(tableCellNode);
221       }
222
223       if (shouldInsertAfter) {
224         targetRowNode.insertAfter(newTableRowNode);
225       } else {
226         targetRowNode.insertBefore(newTableRowNode);
227       }
228     }
229   } else {
230     throw new Error('Row before insertion index does not exist.');
231   }
232
233   return tableNode;
234 }
235
236 const getHeaderState = (
237   currentState: TableCellHeaderState,
238   possibleState: TableCellHeaderState,
239 ): TableCellHeaderState => {
240   if (
241     currentState === TableCellHeaderStates.BOTH ||
242     currentState === possibleState
243   ) {
244     return possibleState;
245   }
246   return TableCellHeaderStates.NO_STATUS;
247 };
248
249 export function $insertTableRow__EXPERIMENTAL(insertAfter = true): void {
250   const selection = $getSelection();
251   invariant(
252     $isRangeSelection(selection) || $isTableSelection(selection),
253     'Expected a RangeSelection or TableSelection',
254   );
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;
260   if (insertAfter) {
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;
269
270         const headerState = getHeaderState(
271           currentCellHeaderState,
272           TableCellHeaderStates.COLUMN,
273         );
274
275         newRow.append(
276           $createTableCellNode(headerState).append($createParagraphNode()),
277         );
278       } else {
279         cell.setRowSpan(cell.__rowSpan + 1);
280       }
281     }
282     const focusEndRowNode = grid.getChildAtIndex(focusEndRow);
283     invariant(
284       $isTableRowNode(focusEndRowNode),
285       'focusEndRow is not a TableRowNode',
286     );
287     focusEndRowNode.insertAfter(newRow);
288   } else {
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;
296
297         const headerState = getHeaderState(
298           currentCellHeaderState,
299           TableCellHeaderStates.COLUMN,
300         );
301
302         newRow.append(
303           $createTableCellNode(headerState).append($createParagraphNode()),
304         );
305       } else {
306         cell.setRowSpan(cell.__rowSpan + 1);
307       }
308     }
309     const focusStartRowNode = grid.getChildAtIndex(focusStartRow);
310     invariant(
311       $isTableRowNode(focusStartRowNode),
312       'focusEndRow is not a TableRowNode',
313     );
314     focusStartRowNode.insertBefore(newRow);
315   }
316 }
317
318 export function $insertTableColumn(
319   tableNode: TableNode,
320   targetIndex: number,
321   shouldInsertAfter = true,
322   columnCount: number,
323   table: TableDOMTable,
324 ): TableNode {
325   const tableRows = tableNode.getChildren();
326
327   const tableCellsToBeInserted = [];
328   for (let r = 0; r < tableRows.length; r++) {
329     const currentTableRowNode = tableRows[r];
330
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');
336         }
337
338         const targetCell = tableRowChildren[targetIndex];
339
340         invariant($isTableCellNode(targetCell), 'Expected table cell');
341
342         const {left, right} = $getTableCellSiblingsFromTableCellNode(
343           targetCell,
344           table,
345         );
346
347         let headerState = TableCellHeaderStates.NO_STATUS;
348
349         if (
350           (left && left.hasHeaderState(TableCellHeaderStates.ROW)) ||
351           (right && right.hasHeaderState(TableCellHeaderStates.ROW))
352         ) {
353           headerState |= TableCellHeaderStates.ROW;
354         }
355
356         const newTableCell = $createTableCellNode(headerState);
357
358         newTableCell.append($createParagraphNode());
359         tableCellsToBeInserted.push({
360           newTableCell,
361           targetCell,
362         });
363       }
364     }
365   }
366   tableCellsToBeInserted.forEach(({newTableCell, targetCell}) => {
367     if (shouldInsertAfter) {
368       targetCell.insertAfter(newTableCell);
369     } else {
370       targetCell.insertBefore(newTableCell);
371     }
372   });
373
374   return tableNode;
375 }
376
377 export function $insertTableColumn__EXPERIMENTAL(insertAfter = true): void {
378   const selection = $getSelection();
379   invariant(
380     $isRangeSelection(selection) || $isTableSelection(selection),
381     'Expected a RangeSelection or TableSelection',
382   );
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(
388     grid,
389     focusCell,
390     anchorCell,
391   );
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
398     : startColumn - 1;
399   const gridFirstChild = grid.getFirstChild();
400   invariant(
401     $isTableRowNode(gridFirstChild),
402     'Expected firstTable child to be a row',
403   );
404   let firstInsertedCell: null | TableCellNode = null;
405   function $createTableCellNodeForInsertTableColumn(
406     headerState: TableCellHeaderState = TableCellHeaderStates.NO_STATUS,
407   ) {
408     const cell = $createTableCellNode(headerState).append(
409       $createParagraphNode(),
410     );
411     if (firstInsertedCell === null) {
412       firstInsertedCell = cell;
413     }
414     return cell;
415   }
416   let loopRow: TableRowNode = gridFirstChild;
417   rowLoop: for (let i = 0; i < rowCount; i++) {
418     if (i !== 0) {
419       const currentRow = loopRow.getNextSibling();
420       invariant(
421         $isTableRowNode(currentRow),
422         'Expected row nextSibling to be a row',
423       );
424       loopRow = currentRow;
425     }
426     const rowMap = gridMap[i];
427
428     const currentCellHeaderState = (
429       rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn]
430         .cell as TableCellNode
431     ).__headerState;
432
433     const headerState = getHeaderState(
434       currentCellHeaderState,
435       TableCellHeaderStates.ROW,
436     );
437
438     if (insertAfterColumn < 0) {
439       $insertFirst(
440         loopRow,
441         $createTableCellNodeForInsertTableColumn(headerState),
442       );
443       continue;
444     }
445     const {
446       cell: currentCell,
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_;
460         } else {
461           loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
462           continue rowLoop;
463         }
464       }
465       insertAfterCell.insertAfter(
466         $createTableCellNodeForInsertTableColumn(headerState),
467       );
468     } else {
469       currentCell.setColSpan(currentCell.__colSpan + 1);
470     }
471   }
472   if (firstInsertedCell !== null) {
473     $moveSelectionToCell(firstInsertedCell);
474   }
475 }
476
477 export function $deleteTableColumn(
478   tableNode: TableNode,
479   targetIndex: number,
480 ): TableNode {
481   const tableRows = tableNode.getChildren();
482
483   for (let i = 0; i < tableRows.length; i++) {
484     const currentTableRowNode = tableRows[i];
485
486     if ($isTableRowNode(currentTableRowNode)) {
487       const tableRowChildren = currentTableRowNode.getChildren();
488
489       if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
490         throw new Error('Table column target index out of range');
491       }
492
493       tableRowChildren[targetIndex].remove();
494     }
495   }
496
497   return tableNode;
498 }
499
500 export function $deleteTableRow__EXPERIMENTAL(): void {
501   const selection = $getSelection();
502   invariant(
503     $isRangeSelection(selection) || $isTableSelection(selection),
504     'Expected a RangeSelection or TableSelection',
505   );
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(
511     grid,
512     anchorCell,
513     focusCell,
514   );
515   const {startRow: anchorStartRow} = anchorCellMap;
516   const {startRow: focusStartRow} = focusCellMap;
517   const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
518   if (gridMap.length === focusEndRow - anchorStartRow + 1) {
519     // Empty grid
520     grid.remove();
521     return;
522   }
523   const columnCount = gridMap[0].length;
524   const nextRow = gridMap[focusEndRow + 1];
525   const nextRowNode: null | TableRowNode = grid.getChildAtIndex(
526     focusEndRow + 1,
527   );
528   for (let row = focusEndRow; row >= anchorStartRow; row--) {
529     for (let column = columnCount - 1; column >= 0; column--) {
530       const {
531         cell,
532         startRow: cellStartRow,
533         startColumn: cellStartColumn,
534       } = gridMap[row][column];
535       if (cellStartColumn !== column) {
536         // Don't repeat work for the same Cell
537         continue;
538       }
539       // Rows overflowing top have to be trimmed
540       if (row === anchorStartRow && cellStartRow < anchorStartRow) {
541         cell.setRowSpan(cell.__rowSpan - (cellStartRow - anchorStartRow));
542       }
543       // Rows overflowing bottom have to be trimmed and moved to the next row
544       if (
545         cellStartRow >= anchorStartRow &&
546         cellStartRow + cell.__rowSpan - 1 > focusEndRow
547       ) {
548         cell.setRowSpan(cell.__rowSpan - (focusEndRow - cellStartRow + 1));
549         invariant(nextRowNode !== null, 'Expected nextRowNode not to be null');
550         if (column === 0) {
551           $insertFirst(nextRowNode, cell);
552         } else {
553           const {cell: previousCell} = nextRow[column - 1];
554           previousCell.insertAfter(cell);
555         }
556       }
557     }
558     const rowNode = grid.getChildAtIndex(row);
559     invariant(
560       $isTableRowNode(rowNode),
561       'Expected GridNode childAtIndex(%s) to be RowNode',
562       String(row),
563     );
564     rowNode.remove();
565   }
566   if (nextRow !== undefined) {
567     const {cell} = nextRow[0];
568     $moveSelectionToCell(cell);
569   } else {
570     const previousRow = gridMap[anchorStartRow - 1];
571     const {cell} = previousRow[0];
572     $moveSelectionToCell(cell);
573   }
574 }
575
576 export function $deleteTableColumn__EXPERIMENTAL(): void {
577   const selection = $getSelection();
578   invariant(
579     $isRangeSelection(selection) || $isTableSelection(selection),
580     'Expected a RangeSelection or TableSelection',
581   );
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(
587     grid,
588     anchorCell,
589     focusCell,
590   );
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,
597   );
598   const selectedColumnCount = endColumn - startColumn + 1;
599   const columnCount = gridMap[0].length;
600   if (columnCount === endColumn - startColumn + 1) {
601     // Empty grid
602     grid.selectPrevious();
603     grid.remove();
604     return;
605   }
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;
613           // Overflowing left
614           cell.setColSpan(
615             cell.__colSpan -
616               // Possible overflow right too
617               Math.min(selectedColumnCount, cell.__colSpan - overflowLeft),
618           );
619         }
620       } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
621         if (column === endColumn) {
622           // Overflowing right
623           const inSelectedArea = endColumn - cellStartColumn + 1;
624           cell.setColSpan(cell.__colSpan - inSelectedArea);
625         }
626       } else {
627         cell.remove();
628       }
629     }
630   }
631   const focusRowMap = gridMap[focusStartRow];
632   const nextColumn =
633     anchorStartColumn > focusStartColumn
634       ? focusRowMap[anchorStartColumn + anchorCell.__colSpan]
635       : focusRowMap[focusStartColumn + focusCell.__colSpan];
636   if (nextColumn !== undefined) {
637     const {cell} = nextColumn;
638     $moveSelectionToCell(cell);
639   } else {
640     const previousRow =
641       focusStartColumn < anchorStartColumn
642         ? focusRowMap[focusStartColumn - 1]
643         : focusRowMap[anchorStartColumn - 1];
644     const {cell} = previousRow;
645     $moveSelectionToCell(cell);
646   }
647 }
648
649 function $moveSelectionToCell(cell: TableCellNode): void {
650   const firstDescendant = cell.getFirstDescendant();
651   if (firstDescendant == null) {
652     cell.selectStart();
653   } else {
654     firstDescendant.getParentOrThrow().selectStart();
655   }
656 }
657
658 function $insertFirst(parent: ElementNode, node: LexicalNode): void {
659   const firstChild = parent.getFirstChild();
660   if (firstChild !== null) {
661     firstChild.insertBefore(node);
662   } else {
663     parent.append(node);
664   }
665 }
666
667 export function $unmergeCell(): void {
668   const selection = $getSelection();
669   invariant(
670     $isRangeSelection(selection) || $isTableSelection(selection),
671     'Expected a RangeSelection or TableSelection',
672   );
673   const anchor = selection.anchor.getNode();
674   const [cell, row, grid] = $getNodeTriplet(anchor);
675   const colSpan = cell.__colSpan;
676   const rowSpan = cell.__rowSpan;
677   if (colSpan > 1) {
678     for (let i = 1; i < colSpan; i++) {
679       cell.insertAfter(
680         $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
681           $createParagraphNode(),
682         ),
683       );
684     }
685     cell.setColSpan(1);
686   }
687   if (rowSpan > 1) {
688     const [map, cellMap] = $computeTableMap(grid, cell, cell);
689     const {startColumn, startRow} = cellMap;
690     let currentRowNode;
691     for (let i = 1; i < rowSpan; i++) {
692       const currentRow = startRow + i;
693       const currentRowMap = map[currentRow];
694       currentRowNode = (currentRowNode || row).getNextSibling();
695       invariant(
696         $isTableRowNode(currentRowNode),
697         'Expected row next sibling to be a row',
698       );
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;
705         }
706         if (currentCell.__colSpan > 1) {
707           column += currentCell.__colSpan - 1;
708         }
709       }
710       if (insertAfterCell === null) {
711         for (let j = 0; j < colSpan; j++) {
712           $insertFirst(
713             currentRowNode,
714             $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
715               $createParagraphNode(),
716             ),
717           );
718         }
719       } else {
720         for (let j = 0; j < colSpan; j++) {
721           insertAfterCell.insertAfter(
722             $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(
723               $createParagraphNode(),
724             ),
725           );
726         }
727       }
728     }
729     cell.setRowSpan(1);
730   }
731 }
732
733 export function $computeTableMap(
734   grid: TableNode,
735   cellA: TableCellNode,
736   cellB: TableCellNode,
737 ): [TableMapType, TableMapValueType, TableMapValueType] {
738   const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(
739     grid,
740     cellA,
741     cellB,
742   );
743   invariant(cellAValue !== null, 'Anchor not found in Grid');
744   invariant(cellBValue !== null, 'Focus not found in Grid');
745   return [tableMap, cellAValue, cellBValue];
746 }
747
748 export function $computeTableMapSkipCellCheck(
749   grid: TableNode,
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) {
757     const value = {
758       cell,
759       startColumn,
760       startRow,
761     };
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] = [];
767       }
768       for (let j = 0; j < colSpan; j++) {
769         tableMap[startRow + i][startColumn + j] = value;
770       }
771     }
772     if (cellA !== null && cellA.is(cell)) {
773       cellAValue = value;
774     }
775     if (cellB !== null && cellB.is(cell)) {
776       cellBValue = value;
777     }
778   }
779   function isEmpty(row: number, column: number) {
780     return tableMap[row] === undefined || tableMap[row][column] === undefined;
781   }
782
783   const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node));
784   for (let i = 0; i < gridChildren.length; i++) {
785     const row = gridChildren[i];
786     invariant(
787       $isTableRowNode(row),
788       'Expected GridNode children to be TableRowNode',
789     );
790     const rowChildren = row.getChildren();
791     let j = 0;
792     for (const cell of rowChildren) {
793       invariant(
794         $isTableCellNode(cell),
795         'Expected TableRowNode children to be TableCellNode',
796       );
797       while (!isEmpty(i, j)) {
798         j++;
799       }
800       write(i, j, cell);
801       j += cell.__colSpan;
802     }
803   }
804   return [tableMap, cellAValue, cellBValue];
805 }
806
807 export function $getNodeTriplet(
808   source: PointType | LexicalNode | TableCellNode,
809 ): [TableCellNode, TableRowNode, TableNode] {
810   let cell: TableCellNode;
811   if (source instanceof TableCellNode) {
812     cell = source;
813   } else if ('__type' in source) {
814     const cell_ = $findMatchingParent(source, $isTableCellNode);
815     invariant(
816       $isTableCellNode(cell_),
817       'Expected to find a parent TableCellNode',
818     );
819     cell = cell_;
820   } else {
821     const cell_ = $findMatchingParent(source.getNode(), $isTableCellNode);
822     invariant(
823       $isTableCellNode(cell_),
824       'Expected to find a parent TableCellNode',
825     );
826     cell = cell_;
827   }
828   const row = cell.getParent();
829   invariant(
830     $isTableRowNode(row),
831     'Expected TableCellNode to have a parent TableRowNode',
832   );
833   const grid = row.getParent();
834   invariant(
835     $isTableNode(grid),
836     'Expected TableRowNode to have a parent GridNode',
837   );
838   return [cell, row, grid];
839 }
840
841 export function $getTableCellNodeRect(tableCellNode: TableCellNode): {
842   rowIndex: number;
843   columnIndex: number;
844   rowSpan: number;
845   colSpan: number;
846 } | null {
847   const [cellNode, , gridNode] = $getNodeTriplet(tableCellNode);
848   const rows = gridNode.getChildren<TableRowNode>();
849   const rowCount = rows.length;
850   const columnCount = rows[0].getChildren().length;
851
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);
856   }
857
858   for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
859     const row = rows[rowIndex];
860     const cells = row.getChildren<TableCellNode>();
861     let columnIndex = 0;
862
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]) {
866         columnIndex++;
867       }
868
869       const cell = cells[cellIndex];
870       const rowSpan = cell.__rowSpan || 1;
871       const colSpan = cell.__colSpan || 1;
872
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;
877         }
878       }
879
880       // Return to the original index, row span and column span of the cell.
881       if (cellNode === cell) {
882         return {
883           colSpan,
884           columnIndex,
885           rowIndex,
886           rowSpan,
887         };
888       }
889
890       columnIndex += colSpan;
891     }
892   }
893
894   return null;
895 }