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