]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts
Lexical: Imported core lexical libs
[bookstack] / resources / js / wysiwyg / lexical / table / LexicalTableSelectionHelpers.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 {TableCellNode} from './LexicalTableCellNode';
10 import type {TableNode} from './LexicalTableNode';
11 import type {TableDOMCell, TableDOMRows} from './LexicalTableObserver';
12 import type {
13   TableMapType,
14   TableMapValueType,
15   TableSelection,
16 } from './LexicalTableSelection';
17 import type {
18   BaseSelection,
19   ElementFormatType,
20   LexicalCommand,
21   LexicalEditor,
22   LexicalNode,
23   RangeSelection,
24   TextFormatType,
25 } from 'lexical';
26
27 import {
28   $getClipboardDataFromSelection,
29   copyToClipboard,
30 } from '@lexical/clipboard';
31 import {$findMatchingParent, objectKlassEquals} from '@lexical/utils';
32 import {
33   $createParagraphNode,
34   $createRangeSelectionFromDom,
35   $createTextNode,
36   $getNearestNodeFromDOMNode,
37   $getPreviousSelection,
38   $getSelection,
39   $isDecoratorNode,
40   $isElementNode,
41   $isRangeSelection,
42   $isRootOrShadowRoot,
43   $isTextNode,
44   $setSelection,
45   COMMAND_PRIORITY_CRITICAL,
46   COMMAND_PRIORITY_HIGH,
47   CONTROLLED_TEXT_INSERTION_COMMAND,
48   CUT_COMMAND,
49   DELETE_CHARACTER_COMMAND,
50   DELETE_LINE_COMMAND,
51   DELETE_WORD_COMMAND,
52   FOCUS_COMMAND,
53   FORMAT_ELEMENT_COMMAND,
54   FORMAT_TEXT_COMMAND,
55   INSERT_PARAGRAPH_COMMAND,
56   KEY_ARROW_DOWN_COMMAND,
57   KEY_ARROW_LEFT_COMMAND,
58   KEY_ARROW_RIGHT_COMMAND,
59   KEY_ARROW_UP_COMMAND,
60   KEY_BACKSPACE_COMMAND,
61   KEY_DELETE_COMMAND,
62   KEY_ESCAPE_COMMAND,
63   KEY_TAB_COMMAND,
64   SELECTION_CHANGE_COMMAND,
65   SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
66 } from 'lexical';
67 import {CAN_USE_DOM} from 'lexical/shared/canUseDOM';
68 import invariant from 'lexical/shared/invariant';
69
70 import {$isTableCellNode} from './LexicalTableCellNode';
71 import {$isTableNode} from './LexicalTableNode';
72 import {TableDOMTable, TableObserver} from './LexicalTableObserver';
73 import {$isTableRowNode} from './LexicalTableRowNode';
74 import {$isTableSelection} from './LexicalTableSelection';
75 import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils';
76
77 const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
78
79 export const getDOMSelection = (
80   targetWindow: Window | null,
81 ): Selection | null =>
82   CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
83
84 const isMouseDownOnEvent = (event: MouseEvent) => {
85   return (event.buttons & 1) === 1;
86 };
87
88 export function applyTableHandlers(
89   tableNode: TableNode,
90   tableElement: HTMLTableElementWithWithTableSelectionState,
91   editor: LexicalEditor,
92   hasTabHandler: boolean,
93 ): TableObserver {
94   const rootElement = editor.getRootElement();
95
96   if (rootElement === null) {
97     throw new Error('No root element.');
98   }
99
100   const tableObserver = new TableObserver(editor, tableNode.getKey());
101   const editorWindow = editor._window || window;
102
103   attachTableObserverToTableElement(tableElement, tableObserver);
104
105   const createMouseHandlers = () => {
106     const onMouseUp = () => {
107       tableObserver.isSelecting = false;
108       editorWindow.removeEventListener('mouseup', onMouseUp);
109       editorWindow.removeEventListener('mousemove', onMouseMove);
110     };
111
112     const onMouseMove = (moveEvent: MouseEvent) => {
113       // delaying mousemove handler to allow selectionchange handler from LexicalEvents.ts to be executed first
114       setTimeout(() => {
115         if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) {
116           tableObserver.isSelecting = false;
117           editorWindow.removeEventListener('mouseup', onMouseUp);
118           editorWindow.removeEventListener('mousemove', onMouseMove);
119           return;
120         }
121         const focusCell = getDOMCellFromTarget(moveEvent.target as Node);
122         if (
123           focusCell !== null &&
124           (tableObserver.anchorX !== focusCell.x ||
125             tableObserver.anchorY !== focusCell.y)
126         ) {
127           moveEvent.preventDefault();
128           tableObserver.setFocusCellForSelection(focusCell);
129         }
130       }, 0);
131     };
132     return {onMouseMove: onMouseMove, onMouseUp: onMouseUp};
133   };
134
135   tableElement.addEventListener('mousedown', (event: MouseEvent) => {
136     setTimeout(() => {
137       if (event.button !== 0) {
138         return;
139       }
140
141       if (!editorWindow) {
142         return;
143       }
144
145       const anchorCell = getDOMCellFromTarget(event.target as Node);
146       if (anchorCell !== null) {
147         stopEvent(event);
148         tableObserver.setAnchorCellForSelection(anchorCell);
149       }
150
151       const {onMouseUp, onMouseMove} = createMouseHandlers();
152       tableObserver.isSelecting = true;
153       editorWindow.addEventListener('mouseup', onMouseUp);
154       editorWindow.addEventListener('mousemove', onMouseMove);
155     }, 0);
156   });
157
158   // Clear selection when clicking outside of dom.
159   const mouseDownCallback = (event: MouseEvent) => {
160     if (event.button !== 0) {
161       return;
162     }
163
164     editor.update(() => {
165       const selection = $getSelection();
166       const target = event.target as Node;
167       if (
168         $isTableSelection(selection) &&
169         selection.tableKey === tableObserver.tableNodeKey &&
170         rootElement.contains(target)
171       ) {
172         tableObserver.clearHighlight();
173       }
174     });
175   };
176
177   editorWindow.addEventListener('mousedown', mouseDownCallback);
178
179   tableObserver.listenersToRemove.add(() =>
180     editorWindow.removeEventListener('mousedown', mouseDownCallback),
181   );
182
183   tableObserver.listenersToRemove.add(
184     editor.registerCommand<KeyboardEvent>(
185       KEY_ARROW_DOWN_COMMAND,
186       (event) =>
187         $handleArrowKey(editor, event, 'down', tableNode, tableObserver),
188       COMMAND_PRIORITY_HIGH,
189     ),
190   );
191
192   tableObserver.listenersToRemove.add(
193     editor.registerCommand<KeyboardEvent>(
194       KEY_ARROW_UP_COMMAND,
195       (event) => $handleArrowKey(editor, event, 'up', tableNode, tableObserver),
196       COMMAND_PRIORITY_HIGH,
197     ),
198   );
199
200   tableObserver.listenersToRemove.add(
201     editor.registerCommand<KeyboardEvent>(
202       KEY_ARROW_LEFT_COMMAND,
203       (event) =>
204         $handleArrowKey(editor, event, 'backward', tableNode, tableObserver),
205       COMMAND_PRIORITY_HIGH,
206     ),
207   );
208
209   tableObserver.listenersToRemove.add(
210     editor.registerCommand<KeyboardEvent>(
211       KEY_ARROW_RIGHT_COMMAND,
212       (event) =>
213         $handleArrowKey(editor, event, 'forward', tableNode, tableObserver),
214       COMMAND_PRIORITY_HIGH,
215     ),
216   );
217
218   tableObserver.listenersToRemove.add(
219     editor.registerCommand<KeyboardEvent>(
220       KEY_ESCAPE_COMMAND,
221       (event) => {
222         const selection = $getSelection();
223         if ($isTableSelection(selection)) {
224           const focusCellNode = $findMatchingParent(
225             selection.focus.getNode(),
226             $isTableCellNode,
227           );
228           if ($isTableCellNode(focusCellNode)) {
229             stopEvent(event);
230             focusCellNode.selectEnd();
231             return true;
232           }
233         }
234
235         return false;
236       },
237       COMMAND_PRIORITY_HIGH,
238     ),
239   );
240
241   const deleteTextHandler = (command: LexicalCommand<boolean>) => () => {
242     const selection = $getSelection();
243
244     if (!$isSelectionInTable(selection, tableNode)) {
245       return false;
246     }
247
248     if ($isTableSelection(selection)) {
249       tableObserver.clearText();
250
251       return true;
252     } else if ($isRangeSelection(selection)) {
253       const tableCellNode = $findMatchingParent(
254         selection.anchor.getNode(),
255         (n) => $isTableCellNode(n),
256       );
257
258       if (!$isTableCellNode(tableCellNode)) {
259         return false;
260       }
261
262       const anchorNode = selection.anchor.getNode();
263       const focusNode = selection.focus.getNode();
264       const isAnchorInside = tableNode.isParentOf(anchorNode);
265       const isFocusInside = tableNode.isParentOf(focusNode);
266
267       const selectionContainsPartialTable =
268         (isAnchorInside && !isFocusInside) ||
269         (isFocusInside && !isAnchorInside);
270
271       if (selectionContainsPartialTable) {
272         tableObserver.clearText();
273         return true;
274       }
275
276       const nearestElementNode = $findMatchingParent(
277         selection.anchor.getNode(),
278         (n) => $isElementNode(n),
279       );
280
281       const topLevelCellElementNode =
282         nearestElementNode &&
283         $findMatchingParent(
284           nearestElementNode,
285           (n) => $isElementNode(n) && $isTableCellNode(n.getParent()),
286         );
287
288       if (
289         !$isElementNode(topLevelCellElementNode) ||
290         !$isElementNode(nearestElementNode)
291       ) {
292         return false;
293       }
294
295       if (
296         command === DELETE_LINE_COMMAND &&
297         topLevelCellElementNode.getPreviousSibling() === null
298       ) {
299         // TODO: Fix Delete Line in Table Cells.
300         return true;
301       }
302     }
303
304     return false;
305   };
306
307   [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND].forEach(
308     (command) => {
309       tableObserver.listenersToRemove.add(
310         editor.registerCommand(
311           command,
312           deleteTextHandler(command),
313           COMMAND_PRIORITY_CRITICAL,
314         ),
315       );
316     },
317   );
318
319   const $deleteCellHandler = (
320     event: KeyboardEvent | ClipboardEvent | null,
321   ): boolean => {
322     const selection = $getSelection();
323
324     if (!$isSelectionInTable(selection, tableNode)) {
325       const nodes = selection ? selection.getNodes() : null;
326       if (nodes) {
327         const table = nodes.find(
328           (node) =>
329             $isTableNode(node) && node.getKey() === tableObserver.tableNodeKey,
330         );
331         if ($isTableNode(table)) {
332           const parentNode = table.getParent();
333           if (!parentNode) {
334             return false;
335           }
336           table.remove();
337         }
338       }
339       return false;
340     }
341
342     if ($isTableSelection(selection)) {
343       if (event) {
344         event.preventDefault();
345         event.stopPropagation();
346       }
347       tableObserver.clearText();
348
349       return true;
350     } else if ($isRangeSelection(selection)) {
351       const tableCellNode = $findMatchingParent(
352         selection.anchor.getNode(),
353         (n) => $isTableCellNode(n),
354       );
355
356       if (!$isTableCellNode(tableCellNode)) {
357         return false;
358       }
359     }
360
361     return false;
362   };
363
364   tableObserver.listenersToRemove.add(
365     editor.registerCommand<KeyboardEvent>(
366       KEY_BACKSPACE_COMMAND,
367       $deleteCellHandler,
368       COMMAND_PRIORITY_CRITICAL,
369     ),
370   );
371
372   tableObserver.listenersToRemove.add(
373     editor.registerCommand<KeyboardEvent>(
374       KEY_DELETE_COMMAND,
375       $deleteCellHandler,
376       COMMAND_PRIORITY_CRITICAL,
377     ),
378   );
379
380   tableObserver.listenersToRemove.add(
381     editor.registerCommand<KeyboardEvent | ClipboardEvent | null>(
382       CUT_COMMAND,
383       (event) => {
384         const selection = $getSelection();
385         if (selection) {
386           if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
387             return false;
388           }
389           // Copying to the clipboard is async so we must capture the data
390           // before we delete it
391           void copyToClipboard(
392             editor,
393             objectKlassEquals(event, ClipboardEvent)
394               ? (event as ClipboardEvent)
395               : null,
396             $getClipboardDataFromSelection(selection),
397           );
398           const intercepted = $deleteCellHandler(event);
399           if ($isRangeSelection(selection)) {
400             selection.removeText();
401           }
402           return intercepted;
403         }
404         return false;
405       },
406       COMMAND_PRIORITY_CRITICAL,
407     ),
408   );
409
410   tableObserver.listenersToRemove.add(
411     editor.registerCommand<TextFormatType>(
412       FORMAT_TEXT_COMMAND,
413       (payload) => {
414         const selection = $getSelection();
415
416         if (!$isSelectionInTable(selection, tableNode)) {
417           return false;
418         }
419
420         if ($isTableSelection(selection)) {
421           tableObserver.formatCells(payload);
422
423           return true;
424         } else if ($isRangeSelection(selection)) {
425           const tableCellNode = $findMatchingParent(
426             selection.anchor.getNode(),
427             (n) => $isTableCellNode(n),
428           );
429
430           if (!$isTableCellNode(tableCellNode)) {
431             return false;
432           }
433         }
434
435         return false;
436       },
437       COMMAND_PRIORITY_CRITICAL,
438     ),
439   );
440
441   tableObserver.listenersToRemove.add(
442     editor.registerCommand<ElementFormatType>(
443       FORMAT_ELEMENT_COMMAND,
444       (formatType) => {
445         const selection = $getSelection();
446         if (
447           !$isTableSelection(selection) ||
448           !$isSelectionInTable(selection, tableNode)
449         ) {
450           return false;
451         }
452
453         const anchorNode = selection.anchor.getNode();
454         const focusNode = selection.focus.getNode();
455         if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
456           return false;
457         }
458
459         const [tableMap, anchorCell, focusCell] = $computeTableMap(
460           tableNode,
461           anchorNode,
462           focusNode,
463         );
464         const maxRow = Math.max(anchorCell.startRow, focusCell.startRow);
465         const maxColumn = Math.max(
466           anchorCell.startColumn,
467           focusCell.startColumn,
468         );
469         const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
470         const minColumn = Math.min(
471           anchorCell.startColumn,
472           focusCell.startColumn,
473         );
474         for (let i = minRow; i <= maxRow; i++) {
475           for (let j = minColumn; j <= maxColumn; j++) {
476             const cell = tableMap[i][j].cell;
477             cell.setFormat(formatType);
478
479             const cellChildren = cell.getChildren();
480             for (let k = 0; k < cellChildren.length; k++) {
481               const child = cellChildren[k];
482               if ($isElementNode(child) && !child.isInline()) {
483                 child.setFormat(formatType);
484               }
485             }
486           }
487         }
488         return true;
489       },
490       COMMAND_PRIORITY_CRITICAL,
491     ),
492   );
493
494   tableObserver.listenersToRemove.add(
495     editor.registerCommand(
496       CONTROLLED_TEXT_INSERTION_COMMAND,
497       (payload) => {
498         const selection = $getSelection();
499
500         if (!$isSelectionInTable(selection, tableNode)) {
501           return false;
502         }
503
504         if ($isTableSelection(selection)) {
505           tableObserver.clearHighlight();
506
507           return false;
508         } else if ($isRangeSelection(selection)) {
509           const tableCellNode = $findMatchingParent(
510             selection.anchor.getNode(),
511             (n) => $isTableCellNode(n),
512           );
513
514           if (!$isTableCellNode(tableCellNode)) {
515             return false;
516           }
517
518           if (typeof payload === 'string') {
519             const edgePosition = $getTableEdgeCursorPosition(
520               editor,
521               selection,
522               tableNode,
523             );
524             if (edgePosition) {
525               $insertParagraphAtTableEdge(edgePosition, tableNode, [
526                 $createTextNode(payload),
527               ]);
528               return true;
529             }
530           }
531         }
532
533         return false;
534       },
535       COMMAND_PRIORITY_CRITICAL,
536     ),
537   );
538
539   if (hasTabHandler) {
540     tableObserver.listenersToRemove.add(
541       editor.registerCommand<KeyboardEvent>(
542         KEY_TAB_COMMAND,
543         (event) => {
544           const selection = $getSelection();
545           if (
546             !$isRangeSelection(selection) ||
547             !selection.isCollapsed() ||
548             !$isSelectionInTable(selection, tableNode)
549           ) {
550             return false;
551           }
552
553           const tableCellNode = $findCellNode(selection.anchor.getNode());
554           if (tableCellNode === null) {
555             return false;
556           }
557
558           stopEvent(event);
559
560           const currentCords = tableNode.getCordsFromCellNode(
561             tableCellNode,
562             tableObserver.table,
563           );
564
565           selectTableNodeInDirection(
566             tableObserver,
567             tableNode,
568             currentCords.x,
569             currentCords.y,
570             !event.shiftKey ? 'forward' : 'backward',
571           );
572
573           return true;
574         },
575         COMMAND_PRIORITY_CRITICAL,
576       ),
577     );
578   }
579
580   tableObserver.listenersToRemove.add(
581     editor.registerCommand(
582       FOCUS_COMMAND,
583       (payload) => {
584         return tableNode.isSelected();
585       },
586       COMMAND_PRIORITY_HIGH,
587     ),
588   );
589
590   function getObserverCellFromCellNode(
591     tableCellNode: TableCellNode,
592   ): TableDOMCell {
593     const currentCords = tableNode.getCordsFromCellNode(
594       tableCellNode,
595       tableObserver.table,
596     );
597     return tableNode.getDOMCellFromCordsOrThrow(
598       currentCords.x,
599       currentCords.y,
600       tableObserver.table,
601     );
602   }
603
604   tableObserver.listenersToRemove.add(
605     editor.registerCommand(
606       SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
607       (selectionPayload) => {
608         const {nodes, selection} = selectionPayload;
609         const anchorAndFocus = selection.getStartEndPoints();
610         const isTableSelection = $isTableSelection(selection);
611         const isRangeSelection = $isRangeSelection(selection);
612         const isSelectionInsideOfGrid =
613           (isRangeSelection &&
614             $findMatchingParent(selection.anchor.getNode(), (n) =>
615               $isTableCellNode(n),
616             ) !== null &&
617             $findMatchingParent(selection.focus.getNode(), (n) =>
618               $isTableCellNode(n),
619             ) !== null) ||
620           isTableSelection;
621
622         if (
623           nodes.length !== 1 ||
624           !$isTableNode(nodes[0]) ||
625           !isSelectionInsideOfGrid ||
626           anchorAndFocus === null
627         ) {
628           return false;
629         }
630         const [anchor] = anchorAndFocus;
631
632         const newGrid = nodes[0];
633         const newGridRows = newGrid.getChildren();
634         const newColumnCount = newGrid
635           .getFirstChildOrThrow<TableNode>()
636           .getChildrenSize();
637         const newRowCount = newGrid.getChildrenSize();
638         const gridCellNode = $findMatchingParent(anchor.getNode(), (n) =>
639           $isTableCellNode(n),
640         );
641         const gridRowNode =
642           gridCellNode &&
643           $findMatchingParent(gridCellNode, (n) => $isTableRowNode(n));
644         const gridNode =
645           gridRowNode &&
646           $findMatchingParent(gridRowNode, (n) => $isTableNode(n));
647
648         if (
649           !$isTableCellNode(gridCellNode) ||
650           !$isTableRowNode(gridRowNode) ||
651           !$isTableNode(gridNode)
652         ) {
653           return false;
654         }
655
656         const startY = gridRowNode.getIndexWithinParent();
657         const stopY = Math.min(
658           gridNode.getChildrenSize() - 1,
659           startY + newRowCount - 1,
660         );
661         const startX = gridCellNode.getIndexWithinParent();
662         const stopX = Math.min(
663           gridRowNode.getChildrenSize() - 1,
664           startX + newColumnCount - 1,
665         );
666         const fromX = Math.min(startX, stopX);
667         const fromY = Math.min(startY, stopY);
668         const toX = Math.max(startX, stopX);
669         const toY = Math.max(startY, stopY);
670         const gridRowNodes = gridNode.getChildren();
671         let newRowIdx = 0;
672
673         for (let r = fromY; r <= toY; r++) {
674           const currentGridRowNode = gridRowNodes[r];
675
676           if (!$isTableRowNode(currentGridRowNode)) {
677             return false;
678           }
679
680           const newGridRowNode = newGridRows[newRowIdx];
681
682           if (!$isTableRowNode(newGridRowNode)) {
683             return false;
684           }
685
686           const gridCellNodes = currentGridRowNode.getChildren();
687           const newGridCellNodes = newGridRowNode.getChildren();
688           let newColumnIdx = 0;
689
690           for (let c = fromX; c <= toX; c++) {
691             const currentGridCellNode = gridCellNodes[c];
692
693             if (!$isTableCellNode(currentGridCellNode)) {
694               return false;
695             }
696
697             const newGridCellNode = newGridCellNodes[newColumnIdx];
698
699             if (!$isTableCellNode(newGridCellNode)) {
700               return false;
701             }
702
703             const originalChildren = currentGridCellNode.getChildren();
704             newGridCellNode.getChildren().forEach((child) => {
705               if ($isTextNode(child)) {
706                 const paragraphNode = $createParagraphNode();
707                 paragraphNode.append(child);
708                 currentGridCellNode.append(child);
709               } else {
710                 currentGridCellNode.append(child);
711               }
712             });
713             originalChildren.forEach((n) => n.remove());
714             newColumnIdx++;
715           }
716
717           newRowIdx++;
718         }
719         return true;
720       },
721       COMMAND_PRIORITY_CRITICAL,
722     ),
723   );
724
725   tableObserver.listenersToRemove.add(
726     editor.registerCommand(
727       SELECTION_CHANGE_COMMAND,
728       () => {
729         const selection = $getSelection();
730         const prevSelection = $getPreviousSelection();
731
732         if ($isRangeSelection(selection)) {
733           const {anchor, focus} = selection;
734           const anchorNode = anchor.getNode();
735           const focusNode = focus.getNode();
736           // Using explicit comparison with table node to ensure it's not a nested table
737           // as in that case we'll leave selection resolving to that table
738           const anchorCellNode = $findCellNode(anchorNode);
739           const focusCellNode = $findCellNode(focusNode);
740           const isAnchorInside = !!(
741             anchorCellNode && tableNode.is($findTableNode(anchorCellNode))
742           );
743           const isFocusInside = !!(
744             focusCellNode && tableNode.is($findTableNode(focusCellNode))
745           );
746           const isPartialyWithinTable = isAnchorInside !== isFocusInside;
747           const isWithinTable = isAnchorInside && isFocusInside;
748           const isBackward = selection.isBackward();
749
750           if (isPartialyWithinTable) {
751             const newSelection = selection.clone();
752             if (isFocusInside) {
753               const [tableMap] = $computeTableMap(
754                 tableNode,
755                 focusCellNode,
756                 focusCellNode,
757               );
758               const firstCell = tableMap[0][0].cell;
759               const lastCell = tableMap[tableMap.length - 1].at(-1)!.cell;
760               newSelection.focus.set(
761                 isBackward ? firstCell.getKey() : lastCell.getKey(),
762                 isBackward
763                   ? firstCell.getChildrenSize()
764                   : lastCell.getChildrenSize(),
765                 'element',
766               );
767             }
768             $setSelection(newSelection);
769             $addHighlightStyleToTable(editor, tableObserver);
770           } else if (isWithinTable) {
771             // Handle case when selection spans across multiple cells but still
772             // has range selection, then we convert it into grid selection
773             if (!anchorCellNode.is(focusCellNode)) {
774               tableObserver.setAnchorCellForSelection(
775                 getObserverCellFromCellNode(anchorCellNode),
776               );
777               tableObserver.setFocusCellForSelection(
778                 getObserverCellFromCellNode(focusCellNode),
779                 true,
780               );
781               if (!tableObserver.isSelecting) {
782                 setTimeout(() => {
783                   const {onMouseUp, onMouseMove} = createMouseHandlers();
784                   tableObserver.isSelecting = true;
785                   editorWindow.addEventListener('mouseup', onMouseUp);
786                   editorWindow.addEventListener('mousemove', onMouseMove);
787                 }, 0);
788               }
789             }
790           }
791         } else if (
792           selection &&
793           $isTableSelection(selection) &&
794           selection.is(prevSelection) &&
795           selection.tableKey === tableNode.getKey()
796         ) {
797           // if selection goes outside of the table we need to change it to Range selection
798           const domSelection = getDOMSelection(editor._window);
799           if (
800             domSelection &&
801             domSelection.anchorNode &&
802             domSelection.focusNode
803           ) {
804             const focusNode = $getNearestNodeFromDOMNode(
805               domSelection.focusNode,
806             );
807             const isFocusOutside =
808               focusNode && !tableNode.is($findTableNode(focusNode));
809
810             const anchorNode = $getNearestNodeFromDOMNode(
811               domSelection.anchorNode,
812             );
813             const isAnchorInside =
814               anchorNode && tableNode.is($findTableNode(anchorNode));
815
816             if (
817               isFocusOutside &&
818               isAnchorInside &&
819               domSelection.rangeCount > 0
820             ) {
821               const newSelection = $createRangeSelectionFromDom(
822                 domSelection,
823                 editor,
824               );
825               if (newSelection) {
826                 newSelection.anchor.set(
827                   tableNode.getKey(),
828                   selection.isBackward() ? tableNode.getChildrenSize() : 0,
829                   'element',
830                 );
831                 domSelection.removeAllRanges();
832                 $setSelection(newSelection);
833               }
834             }
835           }
836         }
837
838         if (
839           selection &&
840           !selection.is(prevSelection) &&
841           ($isTableSelection(selection) || $isTableSelection(prevSelection)) &&
842           tableObserver.tableSelection &&
843           !tableObserver.tableSelection.is(prevSelection)
844         ) {
845           if (
846             $isTableSelection(selection) &&
847             selection.tableKey === tableObserver.tableNodeKey
848           ) {
849             tableObserver.updateTableTableSelection(selection);
850           } else if (
851             !$isTableSelection(selection) &&
852             $isTableSelection(prevSelection) &&
853             prevSelection.tableKey === tableObserver.tableNodeKey
854           ) {
855             tableObserver.updateTableTableSelection(null);
856           }
857           return false;
858         }
859
860         if (
861           tableObserver.hasHijackedSelectionStyles &&
862           !tableNode.isSelected()
863         ) {
864           $removeHighlightStyleToTable(editor, tableObserver);
865         } else if (
866           !tableObserver.hasHijackedSelectionStyles &&
867           tableNode.isSelected()
868         ) {
869           $addHighlightStyleToTable(editor, tableObserver);
870         }
871
872         return false;
873       },
874       COMMAND_PRIORITY_CRITICAL,
875     ),
876   );
877
878   tableObserver.listenersToRemove.add(
879     editor.registerCommand(
880       INSERT_PARAGRAPH_COMMAND,
881       () => {
882         const selection = $getSelection();
883         if (
884           !$isRangeSelection(selection) ||
885           !selection.isCollapsed() ||
886           !$isSelectionInTable(selection, tableNode)
887         ) {
888           return false;
889         }
890         const edgePosition = $getTableEdgeCursorPosition(
891           editor,
892           selection,
893           tableNode,
894         );
895         if (edgePosition) {
896           $insertParagraphAtTableEdge(edgePosition, tableNode);
897           return true;
898         }
899         return false;
900       },
901       COMMAND_PRIORITY_CRITICAL,
902     ),
903   );
904
905   return tableObserver;
906 }
907
908 export type HTMLTableElementWithWithTableSelectionState = HTMLTableElement &
909   Record<typeof LEXICAL_ELEMENT_KEY, TableObserver>;
910
911 export function attachTableObserverToTableElement(
912   tableElement: HTMLTableElementWithWithTableSelectionState,
913   tableObserver: TableObserver,
914 ) {
915   tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
916 }
917
918 export function getTableObserverFromTableElement(
919   tableElement: HTMLTableElementWithWithTableSelectionState,
920 ): TableObserver | null {
921   return tableElement[LEXICAL_ELEMENT_KEY];
922 }
923
924 export function getDOMCellFromTarget(node: Node): TableDOMCell | null {
925   let currentNode: ParentNode | Node | null = node;
926
927   while (currentNode != null) {
928     const nodeName = currentNode.nodeName;
929
930     if (nodeName === 'TD' || nodeName === 'TH') {
931       // @ts-expect-error: internal field
932       const cell = currentNode._cell;
933
934       if (cell === undefined) {
935         return null;
936       }
937
938       return cell;
939     }
940
941     currentNode = currentNode.parentNode;
942   }
943
944   return null;
945 }
946
947 export function doesTargetContainText(node: Node): boolean {
948   const currentNode: ParentNode | Node | null = node;
949
950   if (currentNode !== null) {
951     const nodeName = currentNode.nodeName;
952
953     if (nodeName === 'SPAN') {
954       return true;
955     }
956   }
957   return false;
958 }
959
960 export function getTable(tableElement: HTMLElement): TableDOMTable {
961   const domRows: TableDOMRows = [];
962   const grid = {
963     columns: 0,
964     domRows,
965     rows: 0,
966   };
967   let currentNode = tableElement.firstChild;
968   let x = 0;
969   let y = 0;
970   domRows.length = 0;
971
972   while (currentNode != null) {
973     const nodeMame = currentNode.nodeName;
974
975     if (nodeMame === 'TD' || nodeMame === 'TH') {
976       const elem = currentNode as HTMLElement;
977       const cell = {
978         elem,
979         hasBackgroundColor: elem.style.backgroundColor !== '',
980         highlighted: false,
981         x,
982         y,
983       };
984
985       // @ts-expect-error: internal field
986       currentNode._cell = cell;
987
988       let row = domRows[y];
989       if (row === undefined) {
990         row = domRows[y] = [];
991       }
992
993       row[x] = cell;
994     } else {
995       const child = currentNode.firstChild;
996
997       if (child != null) {
998         currentNode = child;
999         continue;
1000       }
1001     }
1002
1003     const sibling = currentNode.nextSibling;
1004
1005     if (sibling != null) {
1006       x++;
1007       currentNode = sibling;
1008       continue;
1009     }
1010
1011     const parent = currentNode.parentNode;
1012
1013     if (parent != null) {
1014       const parentSibling = parent.nextSibling;
1015
1016       if (parentSibling == null) {
1017         break;
1018       }
1019
1020       y++;
1021       x = 0;
1022       currentNode = parentSibling;
1023     }
1024   }
1025
1026   grid.columns = x + 1;
1027   grid.rows = y + 1;
1028
1029   return grid;
1030 }
1031
1032 export function $updateDOMForSelection(
1033   editor: LexicalEditor,
1034   table: TableDOMTable,
1035   selection: TableSelection | RangeSelection | null,
1036 ) {
1037   const selectedCellNodes = new Set(selection ? selection.getNodes() : []);
1038   $forEachTableCell(table, (cell, lexicalNode) => {
1039     const elem = cell.elem;
1040
1041     if (selectedCellNodes.has(lexicalNode)) {
1042       cell.highlighted = true;
1043       $addHighlightToDOM(editor, cell);
1044     } else {
1045       cell.highlighted = false;
1046       $removeHighlightFromDOM(editor, cell);
1047       if (!elem.getAttribute('style')) {
1048         elem.removeAttribute('style');
1049       }
1050     }
1051   });
1052 }
1053
1054 export function $forEachTableCell(
1055   grid: TableDOMTable,
1056   cb: (
1057     cell: TableDOMCell,
1058     lexicalNode: LexicalNode,
1059     cords: {
1060       x: number;
1061       y: number;
1062     },
1063   ) => void,
1064 ) {
1065   const {domRows} = grid;
1066
1067   for (let y = 0; y < domRows.length; y++) {
1068     const row = domRows[y];
1069     if (!row) {
1070       continue;
1071     }
1072
1073     for (let x = 0; x < row.length; x++) {
1074       const cell = row[x];
1075       if (!cell) {
1076         continue;
1077       }
1078       const lexicalNode = $getNearestNodeFromDOMNode(cell.elem);
1079
1080       if (lexicalNode !== null) {
1081         cb(cell, lexicalNode, {
1082           x,
1083           y,
1084         });
1085       }
1086     }
1087   }
1088 }
1089
1090 export function $addHighlightStyleToTable(
1091   editor: LexicalEditor,
1092   tableSelection: TableObserver,
1093 ) {
1094   tableSelection.disableHighlightStyle();
1095   $forEachTableCell(tableSelection.table, (cell) => {
1096     cell.highlighted = true;
1097     $addHighlightToDOM(editor, cell);
1098   });
1099 }
1100
1101 export function $removeHighlightStyleToTable(
1102   editor: LexicalEditor,
1103   tableObserver: TableObserver,
1104 ) {
1105   tableObserver.enableHighlightStyle();
1106   $forEachTableCell(tableObserver.table, (cell) => {
1107     const elem = cell.elem;
1108     cell.highlighted = false;
1109     $removeHighlightFromDOM(editor, cell);
1110
1111     if (!elem.getAttribute('style')) {
1112       elem.removeAttribute('style');
1113     }
1114   });
1115 }
1116
1117 type Direction = 'backward' | 'forward' | 'up' | 'down';
1118
1119 const selectTableNodeInDirection = (
1120   tableObserver: TableObserver,
1121   tableNode: TableNode,
1122   x: number,
1123   y: number,
1124   direction: Direction,
1125 ): boolean => {
1126   const isForward = direction === 'forward';
1127
1128   switch (direction) {
1129     case 'backward':
1130     case 'forward':
1131       if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
1132         selectTableCellNode(
1133           tableNode.getCellNodeFromCordsOrThrow(
1134             x + (isForward ? 1 : -1),
1135             y,
1136             tableObserver.table,
1137           ),
1138           isForward,
1139         );
1140       } else {
1141         if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {
1142           selectTableCellNode(
1143             tableNode.getCellNodeFromCordsOrThrow(
1144               isForward ? 0 : tableObserver.table.columns - 1,
1145               y + (isForward ? 1 : -1),
1146               tableObserver.table,
1147             ),
1148             isForward,
1149           );
1150         } else if (!isForward) {
1151           tableNode.selectPrevious();
1152         } else {
1153           tableNode.selectNext();
1154         }
1155       }
1156
1157       return true;
1158
1159     case 'up':
1160       if (y !== 0) {
1161         selectTableCellNode(
1162           tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table),
1163           false,
1164         );
1165       } else {
1166         tableNode.selectPrevious();
1167       }
1168
1169       return true;
1170
1171     case 'down':
1172       if (y !== tableObserver.table.rows - 1) {
1173         selectTableCellNode(
1174           tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table),
1175           true,
1176         );
1177       } else {
1178         tableNode.selectNext();
1179       }
1180
1181       return true;
1182     default:
1183       return false;
1184   }
1185 };
1186
1187 const adjustFocusNodeInDirection = (
1188   tableObserver: TableObserver,
1189   tableNode: TableNode,
1190   x: number,
1191   y: number,
1192   direction: Direction,
1193 ): boolean => {
1194   const isForward = direction === 'forward';
1195
1196   switch (direction) {
1197     case 'backward':
1198     case 'forward':
1199       if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
1200         tableObserver.setFocusCellForSelection(
1201           tableNode.getDOMCellFromCordsOrThrow(
1202             x + (isForward ? 1 : -1),
1203             y,
1204             tableObserver.table,
1205           ),
1206         );
1207       }
1208
1209       return true;
1210     case 'up':
1211       if (y !== 0) {
1212         tableObserver.setFocusCellForSelection(
1213           tableNode.getDOMCellFromCordsOrThrow(x, y - 1, tableObserver.table),
1214         );
1215
1216         return true;
1217       } else {
1218         return false;
1219       }
1220     case 'down':
1221       if (y !== tableObserver.table.rows - 1) {
1222         tableObserver.setFocusCellForSelection(
1223           tableNode.getDOMCellFromCordsOrThrow(x, y + 1, tableObserver.table),
1224         );
1225
1226         return true;
1227       } else {
1228         return false;
1229       }
1230     default:
1231       return false;
1232   }
1233 };
1234
1235 function $isSelectionInTable(
1236   selection: null | BaseSelection,
1237   tableNode: TableNode,
1238 ): boolean {
1239   if ($isRangeSelection(selection) || $isTableSelection(selection)) {
1240     const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
1241     const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
1242
1243     return isAnchorInside && isFocusInside;
1244   }
1245
1246   return false;
1247 }
1248
1249 function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) {
1250   if (fromStart) {
1251     tableCell.selectStart();
1252   } else {
1253     tableCell.selectEnd();
1254   }
1255 }
1256
1257 const BROWSER_BLUE_RGB = '172,206,247';
1258 function $addHighlightToDOM(editor: LexicalEditor, cell: TableDOMCell): void {
1259   const element = cell.elem;
1260   const node = $getNearestNodeFromDOMNode(element);
1261   invariant(
1262     $isTableCellNode(node),
1263     'Expected to find LexicalNode from Table Cell DOMNode',
1264   );
1265   const backgroundColor = node.getBackgroundColor();
1266   if (backgroundColor === null) {
1267     element.style.setProperty('background-color', `rgb(${BROWSER_BLUE_RGB})`);
1268   } else {
1269     element.style.setProperty(
1270       'background-image',
1271       `linear-gradient(to right, rgba(${BROWSER_BLUE_RGB},0.85), rgba(${BROWSER_BLUE_RGB},0.85))`,
1272     );
1273   }
1274   element.style.setProperty('caret-color', 'transparent');
1275 }
1276
1277 function $removeHighlightFromDOM(
1278   editor: LexicalEditor,
1279   cell: TableDOMCell,
1280 ): void {
1281   const element = cell.elem;
1282   const node = $getNearestNodeFromDOMNode(element);
1283   invariant(
1284     $isTableCellNode(node),
1285     'Expected to find LexicalNode from Table Cell DOMNode',
1286   );
1287   const backgroundColor = node.getBackgroundColor();
1288   if (backgroundColor === null) {
1289     element.style.removeProperty('background-color');
1290   }
1291   element.style.removeProperty('background-image');
1292   element.style.removeProperty('caret-color');
1293 }
1294
1295 export function $findCellNode(node: LexicalNode): null | TableCellNode {
1296   const cellNode = $findMatchingParent(node, $isTableCellNode);
1297   return $isTableCellNode(cellNode) ? cellNode : null;
1298 }
1299
1300 export function $findTableNode(node: LexicalNode): null | TableNode {
1301   const tableNode = $findMatchingParent(node, $isTableNode);
1302   return $isTableNode(tableNode) ? tableNode : null;
1303 }
1304
1305 function $handleArrowKey(
1306   editor: LexicalEditor,
1307   event: KeyboardEvent,
1308   direction: Direction,
1309   tableNode: TableNode,
1310   tableObserver: TableObserver,
1311 ): boolean {
1312   if (
1313     (direction === 'up' || direction === 'down') &&
1314     isTypeaheadMenuInView(editor)
1315   ) {
1316     return false;
1317   }
1318
1319   const selection = $getSelection();
1320
1321   if (!$isSelectionInTable(selection, tableNode)) {
1322     if ($isRangeSelection(selection)) {
1323       if (selection.isCollapsed() && direction === 'backward') {
1324         const anchorType = selection.anchor.type;
1325         const anchorOffset = selection.anchor.offset;
1326         if (
1327           anchorType !== 'element' &&
1328           !(anchorType === 'text' && anchorOffset === 0)
1329         ) {
1330           return false;
1331         }
1332         const anchorNode = selection.anchor.getNode();
1333         if (!anchorNode) {
1334           return false;
1335         }
1336         const parentNode = $findMatchingParent(
1337           anchorNode,
1338           (n) => $isElementNode(n) && !n.isInline(),
1339         );
1340         if (!parentNode) {
1341           return false;
1342         }
1343         const siblingNode = parentNode.getPreviousSibling();
1344         if (!siblingNode || !$isTableNode(siblingNode)) {
1345           return false;
1346         }
1347         stopEvent(event);
1348         siblingNode.selectEnd();
1349         return true;
1350       } else if (
1351         event.shiftKey &&
1352         (direction === 'up' || direction === 'down')
1353       ) {
1354         const focusNode = selection.focus.getNode();
1355         if ($isRootOrShadowRoot(focusNode)) {
1356           const selectedNode = selection.getNodes()[0];
1357           if (selectedNode) {
1358             const tableCellNode = $findMatchingParent(
1359               selectedNode,
1360               $isTableCellNode,
1361             );
1362             if (tableCellNode && tableNode.isParentOf(tableCellNode)) {
1363               const firstDescendant = tableNode.getFirstDescendant();
1364               const lastDescendant = tableNode.getLastDescendant();
1365               if (!firstDescendant || !lastDescendant) {
1366                 return false;
1367               }
1368               const [firstCellNode] = $getNodeTriplet(firstDescendant);
1369               const [lastCellNode] = $getNodeTriplet(lastDescendant);
1370               const firstCellCoords = tableNode.getCordsFromCellNode(
1371                 firstCellNode,
1372                 tableObserver.table,
1373               );
1374               const lastCellCoords = tableNode.getCordsFromCellNode(
1375                 lastCellNode,
1376                 tableObserver.table,
1377               );
1378               const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(
1379                 firstCellCoords.x,
1380                 firstCellCoords.y,
1381                 tableObserver.table,
1382               );
1383               const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(
1384                 lastCellCoords.x,
1385                 lastCellCoords.y,
1386                 tableObserver.table,
1387               );
1388               tableObserver.setAnchorCellForSelection(firstCellDOM);
1389               tableObserver.setFocusCellForSelection(lastCellDOM, true);
1390               return true;
1391             }
1392           }
1393           return false;
1394         } else {
1395           const focusParentNode = $findMatchingParent(
1396             focusNode,
1397             (n) => $isElementNode(n) && !n.isInline(),
1398           );
1399           if (!focusParentNode) {
1400             return false;
1401           }
1402           const sibling =
1403             direction === 'down'
1404               ? focusParentNode.getNextSibling()
1405               : focusParentNode.getPreviousSibling();
1406           if (
1407             $isTableNode(sibling) &&
1408             tableObserver.tableNodeKey === sibling.getKey()
1409           ) {
1410             const firstDescendant = sibling.getFirstDescendant();
1411             const lastDescendant = sibling.getLastDescendant();
1412             if (!firstDescendant || !lastDescendant) {
1413               return false;
1414             }
1415             const [firstCellNode] = $getNodeTriplet(firstDescendant);
1416             const [lastCellNode] = $getNodeTriplet(lastDescendant);
1417             const newSelection = selection.clone();
1418             newSelection.focus.set(
1419               (direction === 'up' ? firstCellNode : lastCellNode).getKey(),
1420               direction === 'up' ? 0 : lastCellNode.getChildrenSize(),
1421               'element',
1422             );
1423             $setSelection(newSelection);
1424             return true;
1425           }
1426         }
1427       }
1428     }
1429     return false;
1430   }
1431
1432   if ($isRangeSelection(selection) && selection.isCollapsed()) {
1433     const {anchor, focus} = selection;
1434     const anchorCellNode = $findMatchingParent(
1435       anchor.getNode(),
1436       $isTableCellNode,
1437     );
1438     const focusCellNode = $findMatchingParent(
1439       focus.getNode(),
1440       $isTableCellNode,
1441     );
1442     if (
1443       !$isTableCellNode(anchorCellNode) ||
1444       !anchorCellNode.is(focusCellNode)
1445     ) {
1446       return false;
1447     }
1448     const anchorCellTable = $findTableNode(anchorCellNode);
1449     if (anchorCellTable !== tableNode && anchorCellTable != null) {
1450       const anchorCellTableElement = editor.getElementByKey(
1451         anchorCellTable.getKey(),
1452       );
1453       if (anchorCellTableElement != null) {
1454         tableObserver.table = getTable(anchorCellTableElement);
1455         return $handleArrowKey(
1456           editor,
1457           event,
1458           direction,
1459           anchorCellTable,
1460           tableObserver,
1461         );
1462       }
1463     }
1464
1465     if (direction === 'backward' || direction === 'forward') {
1466       const anchorType = anchor.type;
1467       const anchorOffset = anchor.offset;
1468       const anchorNode = anchor.getNode();
1469       if (!anchorNode) {
1470         return false;
1471       }
1472
1473       const selectedNodes = selection.getNodes();
1474       if (selectedNodes.length === 1 && $isDecoratorNode(selectedNodes[0])) {
1475         return false;
1476       }
1477
1478       if (
1479         isExitingTableAnchor(anchorType, anchorOffset, anchorNode, direction)
1480       ) {
1481         return $handleTableExit(event, anchorNode, tableNode, direction);
1482       }
1483
1484       return false;
1485     }
1486
1487     const anchorCellDom = editor.getElementByKey(anchorCellNode.__key);
1488     const anchorDOM = editor.getElementByKey(anchor.key);
1489     if (anchorDOM == null || anchorCellDom == null) {
1490       return false;
1491     }
1492
1493     let edgeSelectionRect;
1494     if (anchor.type === 'element') {
1495       edgeSelectionRect = anchorDOM.getBoundingClientRect();
1496     } else {
1497       const domSelection = window.getSelection();
1498       if (domSelection === null || domSelection.rangeCount === 0) {
1499         return false;
1500       }
1501
1502       const range = domSelection.getRangeAt(0);
1503       edgeSelectionRect = range.getBoundingClientRect();
1504     }
1505
1506     const edgeChild =
1507       direction === 'up'
1508         ? anchorCellNode.getFirstChild()
1509         : anchorCellNode.getLastChild();
1510     if (edgeChild == null) {
1511       return false;
1512     }
1513
1514     const edgeChildDOM = editor.getElementByKey(edgeChild.__key);
1515
1516     if (edgeChildDOM == null) {
1517       return false;
1518     }
1519
1520     const edgeRect = edgeChildDOM.getBoundingClientRect();
1521     const isExiting =
1522       direction === 'up'
1523         ? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height
1524         : edgeSelectionRect.bottom + edgeSelectionRect.height > edgeRect.bottom;
1525
1526     if (isExiting) {
1527       stopEvent(event);
1528
1529       const cords = tableNode.getCordsFromCellNode(
1530         anchorCellNode,
1531         tableObserver.table,
1532       );
1533
1534       if (event.shiftKey) {
1535         const cell = tableNode.getDOMCellFromCordsOrThrow(
1536           cords.x,
1537           cords.y,
1538           tableObserver.table,
1539         );
1540         tableObserver.setAnchorCellForSelection(cell);
1541         tableObserver.setFocusCellForSelection(cell, true);
1542       } else {
1543         return selectTableNodeInDirection(
1544           tableObserver,
1545           tableNode,
1546           cords.x,
1547           cords.y,
1548           direction,
1549         );
1550       }
1551
1552       return true;
1553     }
1554   } else if ($isTableSelection(selection)) {
1555     const {anchor, focus} = selection;
1556     const anchorCellNode = $findMatchingParent(
1557       anchor.getNode(),
1558       $isTableCellNode,
1559     );
1560     const focusCellNode = $findMatchingParent(
1561       focus.getNode(),
1562       $isTableCellNode,
1563     );
1564
1565     const [tableNodeFromSelection] = selection.getNodes();
1566     const tableElement = editor.getElementByKey(
1567       tableNodeFromSelection.getKey(),
1568     );
1569     if (
1570       !$isTableCellNode(anchorCellNode) ||
1571       !$isTableCellNode(focusCellNode) ||
1572       !$isTableNode(tableNodeFromSelection) ||
1573       tableElement == null
1574     ) {
1575       return false;
1576     }
1577     tableObserver.updateTableTableSelection(selection);
1578
1579     const grid = getTable(tableElement);
1580     const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
1581     const anchorCell = tableNode.getDOMCellFromCordsOrThrow(
1582       cordsAnchor.x,
1583       cordsAnchor.y,
1584       grid,
1585     );
1586     tableObserver.setAnchorCellForSelection(anchorCell);
1587
1588     stopEvent(event);
1589
1590     if (event.shiftKey) {
1591       const cords = tableNode.getCordsFromCellNode(focusCellNode, grid);
1592       return adjustFocusNodeInDirection(
1593         tableObserver,
1594         tableNodeFromSelection,
1595         cords.x,
1596         cords.y,
1597         direction,
1598       );
1599     } else {
1600       focusCellNode.selectEnd();
1601     }
1602
1603     return true;
1604   }
1605
1606   return false;
1607 }
1608
1609 function stopEvent(event: Event) {
1610   event.preventDefault();
1611   event.stopImmediatePropagation();
1612   event.stopPropagation();
1613 }
1614
1615 function isTypeaheadMenuInView(editor: LexicalEditor) {
1616   // There is no inbuilt way to check if the component picker is in view
1617   // but we can check if the root DOM element has the aria-controls attribute "typeahead-menu".
1618   const root = editor.getRootElement();
1619   if (!root) {
1620     return false;
1621   }
1622   return (
1623     root.hasAttribute('aria-controls') &&
1624     root.getAttribute('aria-controls') === 'typeahead-menu'
1625   );
1626 }
1627
1628 function isExitingTableAnchor(
1629   type: string,
1630   offset: number,
1631   anchorNode: LexicalNode,
1632   direction: 'backward' | 'forward',
1633 ) {
1634   return (
1635     isExitingTableElementAnchor(type, anchorNode, direction) ||
1636     $isExitingTableTextAnchor(type, offset, anchorNode, direction)
1637   );
1638 }
1639
1640 function isExitingTableElementAnchor(
1641   type: string,
1642   anchorNode: LexicalNode,
1643   direction: 'backward' | 'forward',
1644 ) {
1645   return (
1646     type === 'element' &&
1647     (direction === 'backward'
1648       ? anchorNode.getPreviousSibling() === null
1649       : anchorNode.getNextSibling() === null)
1650   );
1651 }
1652
1653 function $isExitingTableTextAnchor(
1654   type: string,
1655   offset: number,
1656   anchorNode: LexicalNode,
1657   direction: 'backward' | 'forward',
1658 ) {
1659   const parentNode = $findMatchingParent(
1660     anchorNode,
1661     (n) => $isElementNode(n) && !n.isInline(),
1662   );
1663   if (!parentNode) {
1664     return false;
1665   }
1666   const hasValidOffset =
1667     direction === 'backward'
1668       ? offset === 0
1669       : offset === anchorNode.getTextContentSize();
1670   return (
1671     type === 'text' &&
1672     hasValidOffset &&
1673     (direction === 'backward'
1674       ? parentNode.getPreviousSibling() === null
1675       : parentNode.getNextSibling() === null)
1676   );
1677 }
1678
1679 function $handleTableExit(
1680   event: KeyboardEvent,
1681   anchorNode: LexicalNode,
1682   tableNode: TableNode,
1683   direction: 'backward' | 'forward',
1684 ) {
1685   const anchorCellNode = $findMatchingParent(anchorNode, $isTableCellNode);
1686   if (!$isTableCellNode(anchorCellNode)) {
1687     return false;
1688   }
1689   const [tableMap, cellValue] = $computeTableMap(
1690     tableNode,
1691     anchorCellNode,
1692     anchorCellNode,
1693   );
1694   if (!isExitingCell(tableMap, cellValue, direction)) {
1695     return false;
1696   }
1697
1698   const toNode = $getExitingToNode(anchorNode, direction, tableNode);
1699   if (!toNode || $isTableNode(toNode)) {
1700     return false;
1701   }
1702
1703   stopEvent(event);
1704   if (direction === 'backward') {
1705     toNode.selectEnd();
1706   } else {
1707     toNode.selectStart();
1708   }
1709   return true;
1710 }
1711
1712 function isExitingCell(
1713   tableMap: TableMapType,
1714   cellValue: TableMapValueType,
1715   direction: 'backward' | 'forward',
1716 ) {
1717   const firstCell = tableMap[0][0];
1718   const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
1719   const {startColumn, startRow} = cellValue;
1720   return direction === 'backward'
1721     ? startColumn === firstCell.startColumn && startRow === firstCell.startRow
1722     : startColumn === lastCell.startColumn && startRow === lastCell.startRow;
1723 }
1724
1725 function $getExitingToNode(
1726   anchorNode: LexicalNode,
1727   direction: 'backward' | 'forward',
1728   tableNode: TableNode,
1729 ) {
1730   const parentNode = $findMatchingParent(
1731     anchorNode,
1732     (n) => $isElementNode(n) && !n.isInline(),
1733   );
1734   if (!parentNode) {
1735     return undefined;
1736   }
1737   const anchorSibling =
1738     direction === 'backward'
1739       ? parentNode.getPreviousSibling()
1740       : parentNode.getNextSibling();
1741   return anchorSibling && $isTableNode(anchorSibling)
1742     ? anchorSibling
1743     : direction === 'backward'
1744     ? tableNode.getPreviousSibling()
1745     : tableNode.getNextSibling();
1746 }
1747
1748 function $insertParagraphAtTableEdge(
1749   edgePosition: 'first' | 'last',
1750   tableNode: TableNode,
1751   children?: LexicalNode[],
1752 ) {
1753   const paragraphNode = $createParagraphNode();
1754   if (edgePosition === 'first') {
1755     tableNode.insertBefore(paragraphNode);
1756   } else {
1757     tableNode.insertAfter(paragraphNode);
1758   }
1759   paragraphNode.append(...(children || []));
1760   paragraphNode.selectEnd();
1761 }
1762
1763 function $getTableEdgeCursorPosition(
1764   editor: LexicalEditor,
1765   selection: RangeSelection,
1766   tableNode: TableNode,
1767 ) {
1768   const tableNodeParent = tableNode.getParent();
1769   if (!tableNodeParent) {
1770     return undefined;
1771   }
1772
1773   const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
1774   if (!tableNodeParentDOM) {
1775     return undefined;
1776   }
1777
1778   // TODO: Add support for nested tables
1779   const domSelection = window.getSelection();
1780   if (!domSelection || domSelection.anchorNode !== tableNodeParentDOM) {
1781     return undefined;
1782   }
1783
1784   const anchorCellNode = $findMatchingParent(selection.anchor.getNode(), (n) =>
1785     $isTableCellNode(n),
1786   ) as TableCellNode | null;
1787   if (!anchorCellNode) {
1788     return undefined;
1789   }
1790
1791   const parentTable = $findMatchingParent(anchorCellNode, (n) =>
1792     $isTableNode(n),
1793   );
1794   if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {
1795     return undefined;
1796   }
1797
1798   const [tableMap, cellValue] = $computeTableMap(
1799     tableNode,
1800     anchorCellNode,
1801     anchorCellNode,
1802   );
1803   const firstCell = tableMap[0][0];
1804   const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
1805   const {startRow, startColumn} = cellValue;
1806
1807   const isAtFirstCell =
1808     startRow === firstCell.startRow && startColumn === firstCell.startColumn;
1809   const isAtLastCell =
1810     startRow === lastCell.startRow && startColumn === lastCell.startColumn;
1811
1812   if (isAtFirstCell) {
1813     return 'first';
1814   } else if (isAtLastCell) {
1815     return 'last';
1816   } else {
1817     return undefined;
1818   }
1819 }