]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalUtils.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalUtils.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 {
10   CommandPayloadType,
11   EditorConfig,
12   EditorThemeClasses,
13   Klass,
14   LexicalCommand,
15   MutatedNodes,
16   MutationListeners,
17   NodeMutation,
18   RegisteredNode,
19   RegisteredNodes,
20   Spread,
21 } from './LexicalEditor';
22 import type {EditorState} from './LexicalEditorState';
23 import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
24 import type {
25   BaseSelection,
26   PointType,
27   RangeSelection,
28 } from './LexicalSelection';
29 import type {RootNode} from './nodes/LexicalRootNode';
30 import type {TextFormatType, TextNode} from './nodes/LexicalTextNode';
31
32 import {CAN_USE_DOM} from 'lexical/shared/canUseDOM';
33 import {IS_APPLE, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI} from 'lexical/shared/environment';
34 import invariant from 'lexical/shared/invariant';
35 import normalizeClassNames from 'lexical/shared/normalizeClassNames';
36
37 import {
38   $createTextNode,
39   $getPreviousSelection,
40   $getSelection,
41   $isDecoratorNode,
42   $isElementNode,
43   $isLineBreakNode,
44   $isRangeSelection,
45   $isRootNode,
46   $isTextNode,
47   DecoratorNode,
48   ElementNode,
49   LineBreakNode,
50 } from '.';
51 import {
52   COMPOSITION_SUFFIX,
53   DOM_TEXT_TYPE,
54   HAS_DIRTY_NODES,
55   LTR_REGEX,
56   RTL_REGEX,
57   TEXT_TYPE_TO_FORMAT,
58 } from './LexicalConstants';
59 import {LexicalEditor} from './LexicalEditor';
60 import {$flushRootMutations} from './LexicalMutations';
61 import {$normalizeSelection} from './LexicalNormalization';
62 import {
63   errorOnInfiniteTransforms,
64   errorOnReadOnly,
65   getActiveEditor,
66   getActiveEditorState,
67   internalGetActiveEditorState,
68   isCurrentlyReadOnlyMode,
69   triggerCommandListeners,
70   updateEditor,
71 } from './LexicalUpdates';
72
73 export const emptyFunction = () => {
74   return;
75 };
76
77 let keyCounter = 1;
78
79 export function resetRandomKey(): void {
80   keyCounter = 1;
81 }
82
83 export function generateRandomKey(): string {
84   return '' + keyCounter++;
85 }
86
87 export function getRegisteredNodeOrThrow(
88   editor: LexicalEditor,
89   nodeType: string,
90 ): RegisteredNode {
91   const registeredNode = editor._nodes.get(nodeType);
92   if (registeredNode === undefined) {
93     invariant(false, 'registeredNode: Type %s not found', nodeType);
94   }
95   return registeredNode;
96 }
97
98 export const isArray = Array.isArray;
99
100 export const scheduleMicroTask: (fn: () => void) => void =
101   typeof queueMicrotask === 'function'
102     ? queueMicrotask
103     : (fn) => {
104         // No window prefix intended (#1400)
105         Promise.resolve().then(fn);
106       };
107
108 export function $isSelectionCapturedInDecorator(node: Node): boolean {
109   return $isDecoratorNode($getNearestNodeFromDOMNode(node));
110 }
111
112 export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean {
113   const activeElement = document.activeElement as HTMLElement;
114
115   if (activeElement === null) {
116     return false;
117   }
118   const nodeName = activeElement.nodeName;
119
120   return (
121     $isDecoratorNode($getNearestNodeFromDOMNode(anchorDOM)) &&
122     (nodeName === 'INPUT' ||
123       nodeName === 'TEXTAREA' ||
124       (activeElement.contentEditable === 'true' &&
125         getEditorPropertyFromDOMNode(activeElement) == null))
126   );
127 }
128
129 export function isSelectionWithinEditor(
130   editor: LexicalEditor,
131   anchorDOM: null | Node,
132   focusDOM: null | Node,
133 ): boolean {
134   const rootElement = editor.getRootElement();
135   try {
136     return (
137       rootElement !== null &&
138       rootElement.contains(anchorDOM) &&
139       rootElement.contains(focusDOM) &&
140       // Ignore if selection is within nested editor
141       anchorDOM !== null &&
142       !isSelectionCapturedInDecoratorInput(anchorDOM as Node) &&
143       getNearestEditorFromDOMNode(anchorDOM) === editor
144     );
145   } catch (error) {
146     return false;
147   }
148 }
149
150 /**
151  * @returns true if the given argument is a LexicalEditor instance from this build of Lexical
152  */
153 export function isLexicalEditor(editor: unknown): editor is LexicalEditor {
154   // Check instanceof to prevent issues with multiple embedded Lexical installations
155   return editor instanceof LexicalEditor;
156 }
157
158 export function getNearestEditorFromDOMNode(
159   node: Node | null,
160 ): LexicalEditor | null {
161   let currentNode = node;
162   while (currentNode != null) {
163     const editor = getEditorPropertyFromDOMNode(currentNode);
164     if (isLexicalEditor(editor)) {
165       return editor;
166     }
167     currentNode = getParentElement(currentNode);
168   }
169   return null;
170 }
171
172 /** @internal */
173 export function getEditorPropertyFromDOMNode(node: Node | null): unknown {
174   // @ts-expect-error: internal field
175   return node ? node.__lexicalEditor : null;
176 }
177
178 export function getTextDirection(text: string): 'ltr' | 'rtl' | null {
179   if (RTL_REGEX.test(text)) {
180     return 'rtl';
181   }
182   if (LTR_REGEX.test(text)) {
183     return 'ltr';
184   }
185   return null;
186 }
187
188 export function $isTokenOrSegmented(node: TextNode): boolean {
189   return node.isToken() || node.isSegmented();
190 }
191
192 function isDOMNodeLexicalTextNode(node: Node): node is Text {
193   return node.nodeType === DOM_TEXT_TYPE;
194 }
195
196 export function getDOMTextNode(element: Node | null): Text | null {
197   let node = element;
198   while (node != null) {
199     if (isDOMNodeLexicalTextNode(node)) {
200       return node;
201     }
202     node = node.firstChild;
203   }
204   return null;
205 }
206
207 export function toggleTextFormatType(
208   format: number,
209   type: TextFormatType,
210   alignWithFormat: null | number,
211 ): number {
212   const activeFormat = TEXT_TYPE_TO_FORMAT[type];
213   if (
214     alignWithFormat !== null &&
215     (format & activeFormat) === (alignWithFormat & activeFormat)
216   ) {
217     return format;
218   }
219   let newFormat = format ^ activeFormat;
220   if (type === 'subscript') {
221     newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript;
222   } else if (type === 'superscript') {
223     newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript;
224   }
225   return newFormat;
226 }
227
228 export function $isLeafNode(
229   node: LexicalNode | null | undefined,
230 ): node is TextNode | LineBreakNode | DecoratorNode<unknown> {
231   return $isTextNode(node) || $isLineBreakNode(node) || $isDecoratorNode(node);
232 }
233
234 export function $setNodeKey(
235   node: LexicalNode,
236   existingKey: NodeKey | null | undefined,
237 ): void {
238   if (existingKey != null) {
239     if (__DEV__) {
240       errorOnNodeKeyConstructorMismatch(node, existingKey);
241     }
242     node.__key = existingKey;
243     return;
244   }
245   errorOnReadOnly();
246   errorOnInfiniteTransforms();
247   const editor = getActiveEditor();
248   const editorState = getActiveEditorState();
249   const key = generateRandomKey();
250   editorState._nodeMap.set(key, node);
251   // TODO Split this function into leaf/element
252   if ($isElementNode(node)) {
253     editor._dirtyElements.set(key, true);
254   } else {
255     editor._dirtyLeaves.add(key);
256   }
257   editor._cloneNotNeeded.add(key);
258   editor._dirtyType = HAS_DIRTY_NODES;
259   node.__key = key;
260 }
261
262 function errorOnNodeKeyConstructorMismatch(
263   node: LexicalNode,
264   existingKey: NodeKey,
265 ) {
266   const editorState = internalGetActiveEditorState();
267   if (!editorState) {
268     // tests expect to be able to do this kind of clone without an active editor state
269     return;
270   }
271   const existingNode = editorState._nodeMap.get(existingKey);
272   if (existingNode && existingNode.constructor !== node.constructor) {
273     // Lifted condition to if statement because the inverted logic is a bit confusing
274     if (node.constructor.name !== existingNode.constructor.name) {
275       invariant(
276         false,
277         'Lexical node with constructor %s attempted to re-use key from node in active editor state with constructor %s. Keys must not be re-used when the type is changed.',
278         node.constructor.name,
279         existingNode.constructor.name,
280       );
281     } else {
282       invariant(
283         false,
284         'Lexical node with constructor %s attempted to re-use key from node in active editor state with different constructor with the same name (possibly due to invalid Hot Module Replacement). Keys must not be re-used when the type is changed.',
285         node.constructor.name,
286       );
287     }
288   }
289 }
290
291 type IntentionallyMarkedAsDirtyElement = boolean;
292
293 function internalMarkParentElementsAsDirty(
294   parentKey: NodeKey,
295   nodeMap: NodeMap,
296   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
297 ): void {
298   let nextParentKey: string | null = parentKey;
299   while (nextParentKey !== null) {
300     if (dirtyElements.has(nextParentKey)) {
301       return;
302     }
303     const node = nodeMap.get(nextParentKey);
304     if (node === undefined) {
305       break;
306     }
307     dirtyElements.set(nextParentKey, false);
308     nextParentKey = node.__parent;
309   }
310 }
311
312 // TODO #6031 this function or their callers have to adjust selection (i.e. insertBefore)
313 export function removeFromParent(node: LexicalNode): void {
314   const oldParent = node.getParent();
315   if (oldParent !== null) {
316     const writableNode = node.getWritable();
317     const writableParent = oldParent.getWritable();
318     const prevSibling = node.getPreviousSibling();
319     const nextSibling = node.getNextSibling();
320     // TODO: this function duplicates a bunch of operations, can be simplified.
321     if (prevSibling === null) {
322       if (nextSibling !== null) {
323         const writableNextSibling = nextSibling.getWritable();
324         writableParent.__first = nextSibling.__key;
325         writableNextSibling.__prev = null;
326       } else {
327         writableParent.__first = null;
328       }
329     } else {
330       const writablePrevSibling = prevSibling.getWritable();
331       if (nextSibling !== null) {
332         const writableNextSibling = nextSibling.getWritable();
333         writableNextSibling.__prev = writablePrevSibling.__key;
334         writablePrevSibling.__next = writableNextSibling.__key;
335       } else {
336         writablePrevSibling.__next = null;
337       }
338       writableNode.__prev = null;
339     }
340     if (nextSibling === null) {
341       if (prevSibling !== null) {
342         const writablePrevSibling = prevSibling.getWritable();
343         writableParent.__last = prevSibling.__key;
344         writablePrevSibling.__next = null;
345       } else {
346         writableParent.__last = null;
347       }
348     } else {
349       const writableNextSibling = nextSibling.getWritable();
350       if (prevSibling !== null) {
351         const writablePrevSibling = prevSibling.getWritable();
352         writablePrevSibling.__next = writableNextSibling.__key;
353         writableNextSibling.__prev = writablePrevSibling.__key;
354       } else {
355         writableNextSibling.__prev = null;
356       }
357       writableNode.__next = null;
358     }
359     writableParent.__size--;
360     writableNode.__parent = null;
361   }
362 }
363
364 // Never use this function directly! It will break
365 // the cloning heuristic. Instead use node.getWritable().
366 export function internalMarkNodeAsDirty(node: LexicalNode): void {
367   errorOnInfiniteTransforms();
368   const latest = node.getLatest();
369   const parent = latest.__parent;
370   const editorState = getActiveEditorState();
371   const editor = getActiveEditor();
372   const nodeMap = editorState._nodeMap;
373   const dirtyElements = editor._dirtyElements;
374   if (parent !== null) {
375     internalMarkParentElementsAsDirty(parent, nodeMap, dirtyElements);
376   }
377   const key = latest.__key;
378   editor._dirtyType = HAS_DIRTY_NODES;
379   if ($isElementNode(node)) {
380     dirtyElements.set(key, true);
381   } else {
382     // TODO split internally MarkNodeAsDirty into two dedicated Element/leave functions
383     editor._dirtyLeaves.add(key);
384   }
385 }
386
387 export function internalMarkSiblingsAsDirty(node: LexicalNode) {
388   const previousNode = node.getPreviousSibling();
389   const nextNode = node.getNextSibling();
390   if (previousNode !== null) {
391     internalMarkNodeAsDirty(previousNode);
392   }
393   if (nextNode !== null) {
394     internalMarkNodeAsDirty(nextNode);
395   }
396 }
397
398 export function $setCompositionKey(compositionKey: null | NodeKey): void {
399   errorOnReadOnly();
400   const editor = getActiveEditor();
401   const previousCompositionKey = editor._compositionKey;
402   if (compositionKey !== previousCompositionKey) {
403     editor._compositionKey = compositionKey;
404     if (previousCompositionKey !== null) {
405       const node = $getNodeByKey(previousCompositionKey);
406       if (node !== null) {
407         node.getWritable();
408       }
409     }
410     if (compositionKey !== null) {
411       const node = $getNodeByKey(compositionKey);
412       if (node !== null) {
413         node.getWritable();
414       }
415     }
416   }
417 }
418
419 export function $getCompositionKey(): null | NodeKey {
420   if (isCurrentlyReadOnlyMode()) {
421     return null;
422   }
423   const editor = getActiveEditor();
424   return editor._compositionKey;
425 }
426
427 export function $getNodeByKey<T extends LexicalNode>(
428   key: NodeKey,
429   _editorState?: EditorState,
430 ): T | null {
431   const editorState = _editorState || getActiveEditorState();
432   const node = editorState._nodeMap.get(key) as T;
433   if (node === undefined) {
434     return null;
435   }
436   return node;
437 }
438
439 export function $getNodeFromDOMNode(
440   dom: Node,
441   editorState?: EditorState,
442 ): LexicalNode | null {
443   const editor = getActiveEditor();
444   // @ts-ignore We intentionally add this to the Node.
445   const key = dom[`__lexicalKey_${editor._key}`];
446   if (key !== undefined) {
447     return $getNodeByKey(key, editorState);
448   }
449   return null;
450 }
451
452 export function $getNearestNodeFromDOMNode(
453   startingDOM: Node,
454   editorState?: EditorState,
455 ): LexicalNode | null {
456   let dom: Node | null = startingDOM;
457   while (dom != null) {
458     const node = $getNodeFromDOMNode(dom, editorState);
459     if (node !== null) {
460       return node;
461     }
462     dom = getParentElement(dom);
463   }
464   return null;
465 }
466
467 export function cloneDecorators(
468   editor: LexicalEditor,
469 ): Record<NodeKey, unknown> {
470   const currentDecorators = editor._decorators;
471   const pendingDecorators = Object.assign({}, currentDecorators);
472   editor._pendingDecorators = pendingDecorators;
473   return pendingDecorators;
474 }
475
476 export function getEditorStateTextContent(editorState: EditorState): string {
477   return editorState.read(() => $getRoot().getTextContent());
478 }
479
480 export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void {
481   // Mark all existing text nodes as dirty
482   updateEditor(
483     editor,
484     () => {
485       const editorState = getActiveEditorState();
486       if (editorState.isEmpty()) {
487         return;
488       }
489       if (type === 'root') {
490         $getRoot().markDirty();
491         return;
492       }
493       const nodeMap = editorState._nodeMap;
494       for (const [, node] of nodeMap) {
495         node.markDirty();
496       }
497     },
498     editor._pendingEditorState === null
499       ? {
500           tag: 'history-merge',
501         }
502       : undefined,
503   );
504 }
505
506 export function $getRoot(): RootNode {
507   return internalGetRoot(getActiveEditorState());
508 }
509
510 export function internalGetRoot(editorState: EditorState): RootNode {
511   return editorState._nodeMap.get('root') as RootNode;
512 }
513
514 export function $setSelection(selection: null | BaseSelection): void {
515   errorOnReadOnly();
516   const editorState = getActiveEditorState();
517   if (selection !== null) {
518     if (__DEV__) {
519       if (Object.isFrozen(selection)) {
520         invariant(
521           false,
522           '$setSelection called on frozen selection object. Ensure selection is cloned before passing in.',
523         );
524       }
525     }
526     selection.dirty = true;
527     selection.setCachedNodes(null);
528   }
529   editorState._selection = selection;
530 }
531
532 export function $flushMutations(): void {
533   errorOnReadOnly();
534   const editor = getActiveEditor();
535   $flushRootMutations(editor);
536 }
537
538 export function $getNodeFromDOM(dom: Node): null | LexicalNode {
539   const editor = getActiveEditor();
540   const nodeKey = getNodeKeyFromDOM(dom, editor);
541   if (nodeKey === null) {
542     const rootElement = editor.getRootElement();
543     if (dom === rootElement) {
544       return $getNodeByKey('root');
545     }
546     return null;
547   }
548   return $getNodeByKey(nodeKey);
549 }
550
551 export function getTextNodeOffset(
552   node: TextNode,
553   moveSelectionToEnd: boolean,
554 ): number {
555   return moveSelectionToEnd ? node.getTextContentSize() : 0;
556 }
557
558 function getNodeKeyFromDOM(
559   // Note that node here refers to a DOM Node, not an Lexical Node
560   dom: Node,
561   editor: LexicalEditor,
562 ): NodeKey | null {
563   let node: Node | null = dom;
564   while (node != null) {
565     // @ts-ignore We intentionally add this to the Node.
566     const key: NodeKey = node[`__lexicalKey_${editor._key}`];
567     if (key !== undefined) {
568       return key;
569     }
570     node = getParentElement(node);
571   }
572   return null;
573 }
574
575 export function doesContainGrapheme(str: string): boolean {
576   return /[\uD800-\uDBFF][\uDC00-\uDFFF]/g.test(str);
577 }
578
579 export function getEditorsToPropagate(
580   editor: LexicalEditor,
581 ): Array<LexicalEditor> {
582   const editorsToPropagate = [];
583   let currentEditor: LexicalEditor | null = editor;
584   while (currentEditor !== null) {
585     editorsToPropagate.push(currentEditor);
586     currentEditor = currentEditor._parentEditor;
587   }
588   return editorsToPropagate;
589 }
590
591 export function createUID(): string {
592   return Math.random()
593     .toString(36)
594     .replace(/[^a-z]+/g, '')
595     .substr(0, 5);
596 }
597
598 export function getAnchorTextFromDOM(anchorNode: Node): null | string {
599   if (anchorNode.nodeType === DOM_TEXT_TYPE) {
600     return anchorNode.nodeValue;
601   }
602   return null;
603 }
604
605 export function $updateSelectedTextFromDOM(
606   isCompositionEnd: boolean,
607   editor: LexicalEditor,
608   data?: string,
609 ): void {
610   // Update the text content with the latest composition text
611   const domSelection = getDOMSelection(editor._window);
612   if (domSelection === null) {
613     return;
614   }
615   const anchorNode = domSelection.anchorNode;
616   let {anchorOffset, focusOffset} = domSelection;
617   if (anchorNode !== null) {
618     let textContent = getAnchorTextFromDOM(anchorNode);
619     const node = $getNearestNodeFromDOMNode(anchorNode);
620     if (textContent !== null && $isTextNode(node)) {
621       // Data is intentionally truthy, as we check for boolean, null and empty string.
622       if (textContent === COMPOSITION_SUFFIX && data) {
623         const offset = data.length;
624         textContent = data;
625         anchorOffset = offset;
626         focusOffset = offset;
627       }
628
629       if (textContent !== null) {
630         $updateTextNodeFromDOMContent(
631           node,
632           textContent,
633           anchorOffset,
634           focusOffset,
635           isCompositionEnd,
636         );
637       }
638     }
639   }
640 }
641
642 export function $updateTextNodeFromDOMContent(
643   textNode: TextNode,
644   textContent: string,
645   anchorOffset: null | number,
646   focusOffset: null | number,
647   compositionEnd: boolean,
648 ): void {
649   let node = textNode;
650
651   if (node.isAttached() && (compositionEnd || !node.isDirty())) {
652     const isComposing = node.isComposing();
653     let normalizedTextContent = textContent;
654
655     if (
656       (isComposing || compositionEnd) &&
657       textContent[textContent.length - 1] === COMPOSITION_SUFFIX
658     ) {
659       normalizedTextContent = textContent.slice(0, -1);
660     }
661     const prevTextContent = node.getTextContent();
662
663     if (compositionEnd || normalizedTextContent !== prevTextContent) {
664       if (normalizedTextContent === '') {
665         $setCompositionKey(null);
666         if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT) {
667           // For composition (mainly Android), we have to remove the node on a later update
668           const editor = getActiveEditor();
669           setTimeout(() => {
670             editor.update(() => {
671               if (node.isAttached()) {
672                 node.remove();
673               }
674             });
675           }, 20);
676         } else {
677           node.remove();
678         }
679         return;
680       }
681       const parent = node.getParent();
682       const prevSelection = $getPreviousSelection();
683       const prevTextContentSize = node.getTextContentSize();
684       const compositionKey = $getCompositionKey();
685       const nodeKey = node.getKey();
686
687       if (
688         node.isToken() ||
689         (compositionKey !== null &&
690           nodeKey === compositionKey &&
691           !isComposing) ||
692         // Check if character was added at the start or boundaries when not insertable, and we need
693         // to clear this input from occurring as that action wasn't permitted.
694         ($isRangeSelection(prevSelection) &&
695           ((parent !== null &&
696             !parent.canInsertTextBefore() &&
697             prevSelection.anchor.offset === 0) ||
698             (prevSelection.anchor.key === textNode.__key &&
699               prevSelection.anchor.offset === 0 &&
700               !node.canInsertTextBefore() &&
701               !isComposing) ||
702             (prevSelection.focus.key === textNode.__key &&
703               prevSelection.focus.offset === prevTextContentSize &&
704               !node.canInsertTextAfter() &&
705               !isComposing)))
706       ) {
707         node.markDirty();
708         return;
709       }
710       const selection = $getSelection();
711
712       if (
713         !$isRangeSelection(selection) ||
714         anchorOffset === null ||
715         focusOffset === null
716       ) {
717         node.setTextContent(normalizedTextContent);
718         return;
719       }
720       selection.setTextNodeRange(node, anchorOffset, node, focusOffset);
721
722       if (node.isSegmented()) {
723         const originalTextContent = node.getTextContent();
724         const replacement = $createTextNode(originalTextContent);
725         node.replace(replacement);
726         node = replacement;
727       }
728       node.setTextContent(normalizedTextContent);
729     }
730   }
731 }
732
733 function $previousSiblingDoesNotAcceptText(node: TextNode): boolean {
734   const previousSibling = node.getPreviousSibling();
735
736   return (
737     ($isTextNode(previousSibling) ||
738       ($isElementNode(previousSibling) && previousSibling.isInline())) &&
739     !previousSibling.canInsertTextAfter()
740   );
741 }
742
743 // This function is connected to $shouldPreventDefaultAndInsertText and determines whether the
744 // TextNode boundaries are writable or we should use the previous/next sibling instead. For example,
745 // in the case of a LinkNode, boundaries are not writable.
746 export function $shouldInsertTextAfterOrBeforeTextNode(
747   selection: RangeSelection,
748   node: TextNode,
749 ): boolean {
750   if (node.isSegmented()) {
751     return true;
752   }
753   if (!selection.isCollapsed()) {
754     return false;
755   }
756   const offset = selection.anchor.offset;
757   const parent = node.getParentOrThrow();
758   const isToken = node.isToken();
759   if (offset === 0) {
760     return (
761       !node.canInsertTextBefore() ||
762       (!parent.canInsertTextBefore() && !node.isComposing()) ||
763       isToken ||
764       $previousSiblingDoesNotAcceptText(node)
765     );
766   } else if (offset === node.getTextContentSize()) {
767     return (
768       !node.canInsertTextAfter() ||
769       (!parent.canInsertTextAfter() && !node.isComposing()) ||
770       isToken
771     );
772   } else {
773     return false;
774   }
775 }
776
777 export function isTab(
778   key: string,
779   altKey: boolean,
780   ctrlKey: boolean,
781   metaKey: boolean,
782 ): boolean {
783   return key === 'Tab' && !altKey && !ctrlKey && !metaKey;
784 }
785
786 export function isBold(
787   key: string,
788   altKey: boolean,
789   metaKey: boolean,
790   ctrlKey: boolean,
791 ): boolean {
792   return (
793     key.toLowerCase() === 'b' && !altKey && controlOrMeta(metaKey, ctrlKey)
794   );
795 }
796
797 export function isItalic(
798   key: string,
799   altKey: boolean,
800   metaKey: boolean,
801   ctrlKey: boolean,
802 ): boolean {
803   return (
804     key.toLowerCase() === 'i' && !altKey && controlOrMeta(metaKey, ctrlKey)
805   );
806 }
807
808 export function isUnderline(
809   key: string,
810   altKey: boolean,
811   metaKey: boolean,
812   ctrlKey: boolean,
813 ): boolean {
814   return (
815     key.toLowerCase() === 'u' && !altKey && controlOrMeta(metaKey, ctrlKey)
816   );
817 }
818
819 export function isParagraph(key: string, shiftKey: boolean): boolean {
820   return isReturn(key) && !shiftKey;
821 }
822
823 export function isLineBreak(key: string, shiftKey: boolean): boolean {
824   return isReturn(key) && shiftKey;
825 }
826
827 // Inserts a new line after the selection
828
829 export function isOpenLineBreak(key: string, ctrlKey: boolean): boolean {
830   // 79 = KeyO
831   return IS_APPLE && ctrlKey && key.toLowerCase() === 'o';
832 }
833
834 export function isDeleteWordBackward(
835   key: string,
836   altKey: boolean,
837   ctrlKey: boolean,
838 ): boolean {
839   return isBackspace(key) && (IS_APPLE ? altKey : ctrlKey);
840 }
841
842 export function isDeleteWordForward(
843   key: string,
844   altKey: boolean,
845   ctrlKey: boolean,
846 ): boolean {
847   return isDelete(key) && (IS_APPLE ? altKey : ctrlKey);
848 }
849
850 export function isDeleteLineBackward(key: string, metaKey: boolean): boolean {
851   return IS_APPLE && metaKey && isBackspace(key);
852 }
853
854 export function isDeleteLineForward(key: string, metaKey: boolean): boolean {
855   return IS_APPLE && metaKey && isDelete(key);
856 }
857
858 export function isDeleteBackward(
859   key: string,
860   altKey: boolean,
861   metaKey: boolean,
862   ctrlKey: boolean,
863 ): boolean {
864   if (IS_APPLE) {
865     if (altKey || metaKey) {
866       return false;
867     }
868     return isBackspace(key) || (key.toLowerCase() === 'h' && ctrlKey);
869   }
870   if (ctrlKey || altKey || metaKey) {
871     return false;
872   }
873   return isBackspace(key);
874 }
875
876 export function isDeleteForward(
877   key: string,
878   ctrlKey: boolean,
879   shiftKey: boolean,
880   altKey: boolean,
881   metaKey: boolean,
882 ): boolean {
883   if (IS_APPLE) {
884     if (shiftKey || altKey || metaKey) {
885       return false;
886     }
887     return isDelete(key) || (key.toLowerCase() === 'd' && ctrlKey);
888   }
889   if (ctrlKey || altKey || metaKey) {
890     return false;
891   }
892   return isDelete(key);
893 }
894
895 export function isUndo(
896   key: string,
897   shiftKey: boolean,
898   metaKey: boolean,
899   ctrlKey: boolean,
900 ): boolean {
901   return (
902     key.toLowerCase() === 'z' && !shiftKey && controlOrMeta(metaKey, ctrlKey)
903   );
904 }
905
906 export function isRedo(
907   key: string,
908   shiftKey: boolean,
909   metaKey: boolean,
910   ctrlKey: boolean,
911 ): boolean {
912   if (IS_APPLE) {
913     return key.toLowerCase() === 'z' && metaKey && shiftKey;
914   }
915   return (
916     (key.toLowerCase() === 'y' && ctrlKey) ||
917     (key.toLowerCase() === 'z' && ctrlKey && shiftKey)
918   );
919 }
920
921 export function isCopy(
922   key: string,
923   shiftKey: boolean,
924   metaKey: boolean,
925   ctrlKey: boolean,
926 ): boolean {
927   if (shiftKey) {
928     return false;
929   }
930   if (key.toLowerCase() === 'c') {
931     return IS_APPLE ? metaKey : ctrlKey;
932   }
933
934   return false;
935 }
936
937 export function isCut(
938   key: string,
939   shiftKey: boolean,
940   metaKey: boolean,
941   ctrlKey: boolean,
942 ): boolean {
943   if (shiftKey) {
944     return false;
945   }
946   if (key.toLowerCase() === 'x') {
947     return IS_APPLE ? metaKey : ctrlKey;
948   }
949
950   return false;
951 }
952
953 function isArrowLeft(key: string): boolean {
954   return key === 'ArrowLeft';
955 }
956
957 function isArrowRight(key: string): boolean {
958   return key === 'ArrowRight';
959 }
960
961 function isArrowUp(key: string): boolean {
962   return key === 'ArrowUp';
963 }
964
965 function isArrowDown(key: string): boolean {
966   return key === 'ArrowDown';
967 }
968
969 export function isMoveBackward(
970   key: string,
971   ctrlKey: boolean,
972   altKey: boolean,
973   metaKey: boolean,
974 ): boolean {
975   return isArrowLeft(key) && !ctrlKey && !metaKey && !altKey;
976 }
977
978 export function isMoveToStart(
979   key: string,
980   ctrlKey: boolean,
981   shiftKey: boolean,
982   altKey: boolean,
983   metaKey: boolean,
984 ): boolean {
985   return isArrowLeft(key) && !altKey && !shiftKey && (ctrlKey || metaKey);
986 }
987
988 export function isMoveForward(
989   key: string,
990   ctrlKey: boolean,
991   altKey: boolean,
992   metaKey: boolean,
993 ): boolean {
994   return isArrowRight(key) && !ctrlKey && !metaKey && !altKey;
995 }
996
997 export function isMoveToEnd(
998   key: string,
999   ctrlKey: boolean,
1000   shiftKey: boolean,
1001   altKey: boolean,
1002   metaKey: boolean,
1003 ): boolean {
1004   return isArrowRight(key) && !altKey && !shiftKey && (ctrlKey || metaKey);
1005 }
1006
1007 export function isMoveUp(
1008   key: string,
1009   ctrlKey: boolean,
1010   metaKey: boolean,
1011 ): boolean {
1012   return isArrowUp(key) && !ctrlKey && !metaKey;
1013 }
1014
1015 export function isMoveDown(
1016   key: string,
1017   ctrlKey: boolean,
1018   metaKey: boolean,
1019 ): boolean {
1020   return isArrowDown(key) && !ctrlKey && !metaKey;
1021 }
1022
1023 export function isModifier(
1024   ctrlKey: boolean,
1025   shiftKey: boolean,
1026   altKey: boolean,
1027   metaKey: boolean,
1028 ): boolean {
1029   return ctrlKey || shiftKey || altKey || metaKey;
1030 }
1031
1032 export function isSpace(key: string): boolean {
1033   return key === ' ';
1034 }
1035
1036 export function controlOrMeta(metaKey: boolean, ctrlKey: boolean): boolean {
1037   if (IS_APPLE) {
1038     return metaKey;
1039   }
1040   return ctrlKey;
1041 }
1042
1043 export function isReturn(key: string): boolean {
1044   return key === 'Enter';
1045 }
1046
1047 export function isBackspace(key: string): boolean {
1048   return key === 'Backspace';
1049 }
1050
1051 export function isEscape(key: string): boolean {
1052   return key === 'Escape';
1053 }
1054
1055 export function isDelete(key: string): boolean {
1056   return key === 'Delete';
1057 }
1058
1059 export function isSelectAll(
1060   key: string,
1061   metaKey: boolean,
1062   ctrlKey: boolean,
1063 ): boolean {
1064   return key.toLowerCase() === 'a' && controlOrMeta(metaKey, ctrlKey);
1065 }
1066
1067 export function $selectAll(): void {
1068   const root = $getRoot();
1069   const selection = root.select(0, root.getChildrenSize());
1070   $setSelection($normalizeSelection(selection));
1071 }
1072
1073 export function getCachedClassNameArray(
1074   classNamesTheme: EditorThemeClasses,
1075   classNameThemeType: string,
1076 ): Array<string> {
1077   if (classNamesTheme.__lexicalClassNameCache === undefined) {
1078     classNamesTheme.__lexicalClassNameCache = {};
1079   }
1080   const classNamesCache = classNamesTheme.__lexicalClassNameCache;
1081   const cachedClassNames = classNamesCache[classNameThemeType];
1082   if (cachedClassNames !== undefined) {
1083     return cachedClassNames;
1084   }
1085   const classNames = classNamesTheme[classNameThemeType];
1086   // As we're using classList, we need
1087   // to handle className tokens that have spaces.
1088   // The easiest way to do this to convert the
1089   // className tokens to an array that can be
1090   // applied to classList.add()/remove().
1091   if (typeof classNames === 'string') {
1092     const classNamesArr = normalizeClassNames(classNames);
1093     classNamesCache[classNameThemeType] = classNamesArr;
1094     return classNamesArr;
1095   }
1096   return classNames;
1097 }
1098
1099 export function setMutatedNode(
1100   mutatedNodes: MutatedNodes,
1101   registeredNodes: RegisteredNodes,
1102   mutationListeners: MutationListeners,
1103   node: LexicalNode,
1104   mutation: NodeMutation,
1105 ) {
1106   if (mutationListeners.size === 0) {
1107     return;
1108   }
1109   const nodeType = node.__type;
1110   const nodeKey = node.__key;
1111   const registeredNode = registeredNodes.get(nodeType);
1112   if (registeredNode === undefined) {
1113     invariant(false, 'Type %s not in registeredNodes', nodeType);
1114   }
1115   const klass = registeredNode.klass;
1116   let mutatedNodesByType = mutatedNodes.get(klass);
1117   if (mutatedNodesByType === undefined) {
1118     mutatedNodesByType = new Map();
1119     mutatedNodes.set(klass, mutatedNodesByType);
1120   }
1121   const prevMutation = mutatedNodesByType.get(nodeKey);
1122   // If the node has already been "destroyed", yet we are
1123   // re-making it, then this means a move likely happened.
1124   // We should change the mutation to be that of "updated"
1125   // instead.
1126   const isMove = prevMutation === 'destroyed' && mutation === 'created';
1127   if (prevMutation === undefined || isMove) {
1128     mutatedNodesByType.set(nodeKey, isMove ? 'updated' : mutation);
1129   }
1130 }
1131
1132 export function $nodesOfType<T extends LexicalNode>(klass: Klass<T>): Array<T> {
1133   const klassType = klass.getType();
1134   const editorState = getActiveEditorState();
1135   if (editorState._readOnly) {
1136     const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as
1137       | undefined
1138       | Map<string, T>;
1139     return nodes ? Array.from(nodes.values()) : [];
1140   }
1141   const nodes = editorState._nodeMap;
1142   const nodesOfType: Array<T> = [];
1143   for (const [, node] of nodes) {
1144     if (
1145       node instanceof klass &&
1146       node.__type === klassType &&
1147       node.isAttached()
1148     ) {
1149       nodesOfType.push(node as T);
1150     }
1151   }
1152   return nodesOfType;
1153 }
1154
1155 function resolveElement(
1156   element: ElementNode,
1157   isBackward: boolean,
1158   focusOffset: number,
1159 ): LexicalNode | null {
1160   const parent = element.getParent();
1161   let offset = focusOffset;
1162   let block = element;
1163   if (parent !== null) {
1164     if (isBackward && focusOffset === 0) {
1165       offset = block.getIndexWithinParent();
1166       block = parent;
1167     } else if (!isBackward && focusOffset === block.getChildrenSize()) {
1168       offset = block.getIndexWithinParent() + 1;
1169       block = parent;
1170     }
1171   }
1172   return block.getChildAtIndex(isBackward ? offset - 1 : offset);
1173 }
1174
1175 export function $getAdjacentNode(
1176   focus: PointType,
1177   isBackward: boolean,
1178 ): null | LexicalNode {
1179   const focusOffset = focus.offset;
1180   if (focus.type === 'element') {
1181     const block = focus.getNode();
1182     return resolveElement(block, isBackward, focusOffset);
1183   } else {
1184     const focusNode = focus.getNode();
1185     if (
1186       (isBackward && focusOffset === 0) ||
1187       (!isBackward && focusOffset === focusNode.getTextContentSize())
1188     ) {
1189       const possibleNode = isBackward
1190         ? focusNode.getPreviousSibling()
1191         : focusNode.getNextSibling();
1192       if (possibleNode === null) {
1193         return resolveElement(
1194           focusNode.getParentOrThrow(),
1195           isBackward,
1196           focusNode.getIndexWithinParent() + (isBackward ? 0 : 1),
1197         );
1198       }
1199       return possibleNode;
1200     }
1201   }
1202   return null;
1203 }
1204
1205 export function isFirefoxClipboardEvents(editor: LexicalEditor): boolean {
1206   const event = getWindow(editor).event;
1207   const inputType = event && (event as InputEvent).inputType;
1208   return (
1209     inputType === 'insertFromPaste' ||
1210     inputType === 'insertFromPasteAsQuotation'
1211   );
1212 }
1213
1214 export function dispatchCommand<TCommand extends LexicalCommand<unknown>>(
1215   editor: LexicalEditor,
1216   command: TCommand,
1217   payload: CommandPayloadType<TCommand>,
1218 ): boolean {
1219   return triggerCommandListeners(editor, command, payload);
1220 }
1221
1222 export function $textContentRequiresDoubleLinebreakAtEnd(
1223   node: ElementNode,
1224 ): boolean {
1225   return !$isRootNode(node) && !node.isLastChild() && !node.isInline();
1226 }
1227
1228 export function getElementByKeyOrThrow(
1229   editor: LexicalEditor,
1230   key: NodeKey,
1231 ): HTMLElement {
1232   const element = editor._keyToDOMMap.get(key);
1233
1234   if (element === undefined) {
1235     invariant(
1236       false,
1237       'Reconciliation: could not find DOM element for node key %s',
1238       key,
1239     );
1240   }
1241
1242   return element;
1243 }
1244
1245 export function getParentElement(node: Node): HTMLElement | null {
1246   const parentElement =
1247     (node as HTMLSlotElement).assignedSlot || node.parentElement;
1248   return parentElement !== null && parentElement.nodeType === 11
1249     ? ((parentElement as unknown as ShadowRoot).host as HTMLElement)
1250     : parentElement;
1251 }
1252
1253 export function scrollIntoViewIfNeeded(
1254   editor: LexicalEditor,
1255   selectionRect: DOMRect,
1256   rootElement: HTMLElement,
1257 ): void {
1258   const doc = rootElement.ownerDocument;
1259   const defaultView = doc.defaultView;
1260
1261   if (defaultView === null) {
1262     return;
1263   }
1264   let {top: currentTop, bottom: currentBottom} = selectionRect;
1265   let targetTop = 0;
1266   let targetBottom = 0;
1267   let element: HTMLElement | null = rootElement;
1268
1269   while (element !== null) {
1270     const isBodyElement = element === doc.body;
1271     if (isBodyElement) {
1272       targetTop = 0;
1273       targetBottom = getWindow(editor).innerHeight;
1274     } else {
1275       const targetRect = element.getBoundingClientRect();
1276       targetTop = targetRect.top;
1277       targetBottom = targetRect.bottom;
1278     }
1279     let diff = 0;
1280
1281     if (currentTop < targetTop) {
1282       diff = -(targetTop - currentTop);
1283     } else if (currentBottom > targetBottom) {
1284       diff = currentBottom - targetBottom;
1285     }
1286
1287     if (diff !== 0) {
1288       if (isBodyElement) {
1289         // Only handles scrolling of Y axis
1290         defaultView.scrollBy(0, diff);
1291       } else {
1292         const scrollTop = element.scrollTop;
1293         element.scrollTop += diff;
1294         const yOffset = element.scrollTop - scrollTop;
1295         currentTop -= yOffset;
1296         currentBottom -= yOffset;
1297       }
1298     }
1299     if (isBodyElement) {
1300       break;
1301     }
1302     element = getParentElement(element);
1303   }
1304 }
1305
1306 export function $hasUpdateTag(tag: string): boolean {
1307   const editor = getActiveEditor();
1308   return editor._updateTags.has(tag);
1309 }
1310
1311 export function $addUpdateTag(tag: string): void {
1312   errorOnReadOnly();
1313   const editor = getActiveEditor();
1314   editor._updateTags.add(tag);
1315 }
1316
1317 export function $maybeMoveChildrenSelectionToParent(
1318   parentNode: LexicalNode,
1319 ): BaseSelection | null {
1320   const selection = $getSelection();
1321   if (!$isRangeSelection(selection) || !$isElementNode(parentNode)) {
1322     return selection;
1323   }
1324   const {anchor, focus} = selection;
1325   const anchorNode = anchor.getNode();
1326   const focusNode = focus.getNode();
1327   if ($hasAncestor(anchorNode, parentNode)) {
1328     anchor.set(parentNode.__key, 0, 'element');
1329   }
1330   if ($hasAncestor(focusNode, parentNode)) {
1331     focus.set(parentNode.__key, 0, 'element');
1332   }
1333   return selection;
1334 }
1335
1336 export function $hasAncestor(
1337   child: LexicalNode,
1338   targetNode: LexicalNode,
1339 ): boolean {
1340   let parent = child.getParent();
1341   while (parent !== null) {
1342     if (parent.is(targetNode)) {
1343       return true;
1344     }
1345     parent = parent.getParent();
1346   }
1347   return false;
1348 }
1349
1350 export function getDefaultView(domElem: HTMLElement): Window | null {
1351   const ownerDoc = domElem.ownerDocument;
1352   return (ownerDoc && ownerDoc.defaultView) || null;
1353 }
1354
1355 export function getWindow(editor: LexicalEditor): Window {
1356   const windowObj = editor._window;
1357   if (windowObj === null) {
1358     invariant(false, 'window object not found');
1359   }
1360   return windowObj;
1361 }
1362
1363 export function $isInlineElementOrDecoratorNode(node: LexicalNode): boolean {
1364   return (
1365     ($isElementNode(node) && node.isInline()) ||
1366     ($isDecoratorNode(node) && node.isInline())
1367   );
1368 }
1369
1370 export function $getNearestRootOrShadowRoot(
1371   node: LexicalNode,
1372 ): RootNode | ElementNode {
1373   let parent = node.getParentOrThrow();
1374   while (parent !== null) {
1375     if ($isRootOrShadowRoot(parent)) {
1376       return parent;
1377     }
1378     parent = parent.getParentOrThrow();
1379   }
1380   return parent;
1381 }
1382
1383 const ShadowRootNodeBrand: unique symbol = Symbol.for(
1384   '@lexical/ShadowRootNodeBrand',
1385 );
1386 type ShadowRootNode = Spread<
1387   {isShadowRoot(): true; [ShadowRootNodeBrand]: never},
1388   ElementNode
1389 >;
1390 export function $isRootOrShadowRoot(
1391   node: null | LexicalNode,
1392 ): node is RootNode | ShadowRootNode {
1393   return $isRootNode(node) || ($isElementNode(node) && node.isShadowRoot());
1394 }
1395
1396 /**
1397  * Returns a shallow clone of node with a new key
1398  *
1399  * @param node - The node to be copied.
1400  * @returns The copy of the node.
1401  */
1402 export function $copyNode<T extends LexicalNode>(node: T): T {
1403   const copy = node.constructor.clone(node) as T;
1404   $setNodeKey(copy, null);
1405   return copy;
1406 }
1407
1408 export function $applyNodeReplacement<N extends LexicalNode>(
1409   node: LexicalNode,
1410 ): N {
1411   const editor = getActiveEditor();
1412   const nodeType = node.constructor.getType();
1413   const registeredNode = editor._nodes.get(nodeType);
1414   if (registeredNode === undefined) {
1415     invariant(
1416       false,
1417       '$initializeNode failed. Ensure node has been registered to the editor. You can do this by passing the node class via the "nodes" array in the editor config.',
1418     );
1419   }
1420   const replaceFunc = registeredNode.replace;
1421   if (replaceFunc !== null) {
1422     const replacementNode = replaceFunc(node) as N;
1423     if (!(replacementNode instanceof node.constructor)) {
1424       invariant(
1425         false,
1426         '$initializeNode failed. Ensure replacement node is a subclass of the original node.',
1427       );
1428     }
1429     return replacementNode;
1430   }
1431   return node as N;
1432 }
1433
1434 export function errorOnInsertTextNodeOnRoot(
1435   node: LexicalNode,
1436   insertNode: LexicalNode,
1437 ): void {
1438   const parentNode = node.getParent();
1439   if (
1440     $isRootNode(parentNode) &&
1441     !$isElementNode(insertNode) &&
1442     !$isDecoratorNode(insertNode)
1443   ) {
1444     invariant(
1445       false,
1446       'Only element or decorator nodes can be inserted in to the root node',
1447     );
1448   }
1449 }
1450
1451 export function $getNodeByKeyOrThrow<N extends LexicalNode>(key: NodeKey): N {
1452   const node = $getNodeByKey<N>(key);
1453   if (node === null) {
1454     invariant(
1455       false,
1456       "Expected node with key %s to exist but it's not in the nodeMap.",
1457       key,
1458     );
1459   }
1460   return node;
1461 }
1462
1463 function createBlockCursorElement(editorConfig: EditorConfig): HTMLDivElement {
1464   const theme = editorConfig.theme;
1465   const element = document.createElement('div');
1466   element.contentEditable = 'false';
1467   element.setAttribute('data-lexical-cursor', 'true');
1468   let blockCursorTheme = theme.blockCursor;
1469   if (blockCursorTheme !== undefined) {
1470     if (typeof blockCursorTheme === 'string') {
1471       const classNamesArr = normalizeClassNames(blockCursorTheme);
1472       // @ts-expect-error: intentional
1473       blockCursorTheme = theme.blockCursor = classNamesArr;
1474     }
1475     if (blockCursorTheme !== undefined) {
1476       element.classList.add(...blockCursorTheme);
1477     }
1478   }
1479   return element;
1480 }
1481
1482 function needsBlockCursor(node: null | LexicalNode): boolean {
1483   return (
1484     ($isDecoratorNode(node) || ($isElementNode(node) && !node.canBeEmpty())) &&
1485     !node.isInline()
1486   );
1487 }
1488
1489 export function removeDOMBlockCursorElement(
1490   blockCursorElement: HTMLElement,
1491   editor: LexicalEditor,
1492   rootElement: HTMLElement,
1493 ) {
1494   rootElement.style.removeProperty('caret-color');
1495   editor._blockCursorElement = null;
1496   const parentElement = blockCursorElement.parentElement;
1497   if (parentElement !== null) {
1498     parentElement.removeChild(blockCursorElement);
1499   }
1500 }
1501
1502 export function updateDOMBlockCursorElement(
1503   editor: LexicalEditor,
1504   rootElement: HTMLElement,
1505   nextSelection: null | BaseSelection,
1506 ): void {
1507   let blockCursorElement = editor._blockCursorElement;
1508
1509   if (
1510     $isRangeSelection(nextSelection) &&
1511     nextSelection.isCollapsed() &&
1512     nextSelection.anchor.type === 'element' &&
1513     rootElement.contains(document.activeElement)
1514   ) {
1515     const anchor = nextSelection.anchor;
1516     const elementNode = anchor.getNode();
1517     const offset = anchor.offset;
1518     const elementNodeSize = elementNode.getChildrenSize();
1519     let isBlockCursor = false;
1520     let insertBeforeElement: null | HTMLElement = null;
1521
1522     if (offset === elementNodeSize) {
1523       const child = elementNode.getChildAtIndex(offset - 1);
1524       if (needsBlockCursor(child)) {
1525         isBlockCursor = true;
1526       }
1527     } else {
1528       const child = elementNode.getChildAtIndex(offset);
1529       if (needsBlockCursor(child)) {
1530         const sibling = (child as LexicalNode).getPreviousSibling();
1531         if (sibling === null || needsBlockCursor(sibling)) {
1532           isBlockCursor = true;
1533           insertBeforeElement = editor.getElementByKey(
1534             (child as LexicalNode).__key,
1535           );
1536         }
1537       }
1538     }
1539     if (isBlockCursor) {
1540       const elementDOM = editor.getElementByKey(
1541         elementNode.__key,
1542       ) as HTMLElement;
1543       if (blockCursorElement === null) {
1544         editor._blockCursorElement = blockCursorElement =
1545           createBlockCursorElement(editor._config);
1546       }
1547       rootElement.style.caretColor = 'transparent';
1548       if (insertBeforeElement === null) {
1549         elementDOM.appendChild(blockCursorElement);
1550       } else {
1551         elementDOM.insertBefore(blockCursorElement, insertBeforeElement);
1552       }
1553       return;
1554     }
1555   }
1556   // Remove cursor
1557   if (blockCursorElement !== null) {
1558     removeDOMBlockCursorElement(blockCursorElement, editor, rootElement);
1559   }
1560 }
1561
1562 export function getDOMSelection(targetWindow: null | Window): null | Selection {
1563   return !CAN_USE_DOM ? null : (targetWindow || window).getSelection();
1564 }
1565
1566 export function $splitNode(
1567   node: ElementNode,
1568   offset: number,
1569 ): [ElementNode | null, ElementNode] {
1570   let startNode = node.getChildAtIndex(offset);
1571   if (startNode == null) {
1572     startNode = node;
1573   }
1574
1575   invariant(
1576     !$isRootOrShadowRoot(node),
1577     'Can not call $splitNode() on root element',
1578   );
1579
1580   const recurse = <T extends LexicalNode>(
1581     currentNode: T,
1582   ): [ElementNode, ElementNode, T] => {
1583     const parent = currentNode.getParentOrThrow();
1584     const isParentRoot = $isRootOrShadowRoot(parent);
1585     // The node we start split from (leaf) is moved, but its recursive
1586     // parents are copied to create separate tree
1587     const nodeToMove =
1588       currentNode === startNode && !isParentRoot
1589         ? currentNode
1590         : $copyNode(currentNode);
1591
1592     if (isParentRoot) {
1593       invariant(
1594         $isElementNode(currentNode) && $isElementNode(nodeToMove),
1595         'Children of a root must be ElementNode',
1596       );
1597
1598       currentNode.insertAfter(nodeToMove);
1599       return [currentNode, nodeToMove, nodeToMove];
1600     } else {
1601       const [leftTree, rightTree, newParent] = recurse(parent);
1602       const nextSiblings = currentNode.getNextSiblings();
1603
1604       newParent.append(nodeToMove, ...nextSiblings);
1605       return [leftTree, rightTree, nodeToMove];
1606     }
1607   };
1608
1609   const [leftTree, rightTree] = recurse(startNode);
1610
1611   return [leftTree, rightTree];
1612 }
1613
1614 export function $findMatchingParent(
1615   startingNode: LexicalNode,
1616   findFn: (node: LexicalNode) => boolean,
1617 ): LexicalNode | null {
1618   let curr: ElementNode | LexicalNode | null = startingNode;
1619
1620   while (curr !== $getRoot() && curr != null) {
1621     if (findFn(curr)) {
1622       return curr;
1623     }
1624
1625     curr = curr.getParent();
1626   }
1627
1628   return null;
1629 }
1630
1631 /**
1632  * @param x - The element being tested
1633  * @returns Returns true if x is an HTML anchor tag, false otherwise
1634  */
1635 export function isHTMLAnchorElement(x: Node): x is HTMLAnchorElement {
1636   return isHTMLElement(x) && x.tagName === 'A';
1637 }
1638
1639 /**
1640  * @param x - The element being testing
1641  * @returns Returns true if x is an HTML element, false otherwise.
1642  */
1643 export function isHTMLElement(x: Node | EventTarget): x is HTMLElement {
1644   // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors
1645   return x.nodeType === 1;
1646 }
1647
1648 /**
1649  *
1650  * @param node - the Dom Node to check
1651  * @returns if the Dom Node is an inline node
1652  */
1653 export function isInlineDomNode(node: Node) {
1654   const inlineNodes = new RegExp(
1655     /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var|#text)$/,
1656     'i',
1657   );
1658   return node.nodeName.match(inlineNodes) !== null;
1659 }
1660
1661 /**
1662  *
1663  * @param node - the Dom Node to check
1664  * @returns if the Dom Node is a block node
1665  */
1666 export function isBlockDomNode(node: Node) {
1667   const blockNodes = new RegExp(
1668     /^(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|td|tfoot|ul|video)$/,
1669     'i',
1670   );
1671   return node.nodeName.match(blockNodes) !== null;
1672 }
1673
1674 /**
1675  * This function is for internal use of the library.
1676  * Please do not use it as it may change in the future.
1677  */
1678 export function INTERNAL_$isBlock(
1679   node: LexicalNode,
1680 ): node is ElementNode | DecoratorNode<unknown> {
1681   if ($isRootNode(node) || ($isDecoratorNode(node) && !node.isInline())) {
1682     return true;
1683   }
1684   if (!$isElementNode(node) || $isRootOrShadowRoot(node)) {
1685     return false;
1686   }
1687
1688   const firstChild = node.getFirstChild();
1689   const isLeafElement =
1690     firstChild === null ||
1691     $isLineBreakNode(firstChild) ||
1692     $isTextNode(firstChild) ||
1693     firstChild.isInline();
1694
1695   return !node.isInline() && node.canBeEmpty() !== false && isLeafElement;
1696 }
1697
1698 export function $getAncestor<NodeType extends LexicalNode = LexicalNode>(
1699   node: LexicalNode,
1700   predicate: (ancestor: LexicalNode) => ancestor is NodeType,
1701 ) {
1702   let parent = node;
1703   while (parent !== null && parent.getParent() !== null && !predicate(parent)) {
1704     parent = parent.getParentOrThrow();
1705   }
1706   return predicate(parent) ? parent : null;
1707 }
1708
1709 /**
1710  * Utility function for accessing current active editor instance.
1711  * @returns Current active editor
1712  */
1713 export function $getEditor(): LexicalEditor {
1714   return getActiveEditor();
1715 }
1716
1717 /** @internal */
1718 export type TypeToNodeMap = Map<string, NodeMap>;
1719 /**
1720  * @internal
1721  * Compute a cached Map of node type to nodes for a frozen EditorState
1722  */
1723 const cachedNodeMaps = new WeakMap<EditorState, TypeToNodeMap>();
1724 const EMPTY_TYPE_TO_NODE_MAP: TypeToNodeMap = new Map();
1725 export function getCachedTypeToNodeMap(
1726   editorState: EditorState,
1727 ): TypeToNodeMap {
1728   // If this is a new Editor it may have a writable this._editorState
1729   // with only a 'root' entry.
1730   if (!editorState._readOnly && editorState.isEmpty()) {
1731     return EMPTY_TYPE_TO_NODE_MAP;
1732   }
1733   invariant(
1734     editorState._readOnly,
1735     'getCachedTypeToNodeMap called with a writable EditorState',
1736   );
1737   let typeToNodeMap = cachedNodeMaps.get(editorState);
1738   if (!typeToNodeMap) {
1739     typeToNodeMap = new Map();
1740     cachedNodeMaps.set(editorState, typeToNodeMap);
1741     for (const [nodeKey, node] of editorState._nodeMap) {
1742       const nodeType = node.__type;
1743       let nodeMap = typeToNodeMap.get(nodeType);
1744       if (!nodeMap) {
1745         nodeMap = new Map();
1746         typeToNodeMap.set(nodeType, nodeMap);
1747       }
1748       nodeMap.set(nodeKey, node);
1749     }
1750   }
1751   return typeToNodeMap;
1752 }
1753
1754 /**
1755  * Returns a clone of a node using `node.constructor.clone()` followed by
1756  * `clone.afterCloneFrom(node)`. The resulting clone must have the same key,
1757  * parent/next/prev pointers, and other properties that are not set by
1758  * `node.constructor.clone` (format, style, etc.). This is primarily used by
1759  * {@link LexicalNode.getWritable} to create a writable version of an
1760  * existing node. The clone is the same logical node as the original node,
1761  * do not try and use this function to duplicate or copy an existing node.
1762  *
1763  * Does not mutate the EditorState.
1764  * @param node - The node to be cloned.
1765  * @returns The clone of the node.
1766  */
1767 export function $cloneWithProperties<T extends LexicalNode>(latestNode: T): T {
1768   const constructor = latestNode.constructor;
1769   const mutableNode = constructor.clone(latestNode) as T;
1770   mutableNode.afterCloneFrom(latestNode);
1771   if (__DEV__) {
1772     invariant(
1773       mutableNode.__key === latestNode.__key,
1774       "$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor",
1775       constructor.name,
1776       constructor.getType(),
1777     );
1778     invariant(
1779       mutableNode.__parent === latestNode.__parent &&
1780         mutableNode.__next === latestNode.__next &&
1781         mutableNode.__prev === latestNode.__prev,
1782       "$cloneWithProperties: %s.clone(node) (with type '%s') overrided afterCloneFrom but did not call super.afterCloneFrom(prevNode)",
1783       constructor.name,
1784       constructor.getType(),
1785     );
1786   }
1787   return mutableNode;
1788 }