]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalSelection.ts
Lexical: Imported core lexical libs
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalSelection.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 {LexicalEditor} from './LexicalEditor';
10 import type {EditorState} from './LexicalEditorState';
11 import type {NodeKey} from './LexicalNode';
12 import type {ElementNode} from './nodes/LexicalElementNode';
13 import type {TextFormatType} from './nodes/LexicalTextNode';
14
15 import invariant from 'lexical/shared/invariant';
16
17 import {
18   $createLineBreakNode,
19   $createParagraphNode,
20   $createTextNode,
21   $isDecoratorNode,
22   $isElementNode,
23   $isLineBreakNode,
24   $isRootNode,
25   $isTextNode,
26   $setSelection,
27   SELECTION_CHANGE_COMMAND,
28   TextNode,
29 } from '.';
30 import {DOM_ELEMENT_TYPE, TEXT_TYPE_TO_FORMAT} from './LexicalConstants';
31 import {
32   markCollapsedSelectionFormat,
33   markSelectionChangeFromDOMUpdate,
34 } from './LexicalEvents';
35 import {getIsProcessingMutations} from './LexicalMutations';
36 import {insertRangeAfter, LexicalNode} from './LexicalNode';
37 import {
38   getActiveEditor,
39   getActiveEditorState,
40   isCurrentlyReadOnlyMode,
41 } from './LexicalUpdates';
42 import {
43   $getAdjacentNode,
44   $getAncestor,
45   $getCompositionKey,
46   $getNearestRootOrShadowRoot,
47   $getNodeByKey,
48   $getNodeFromDOM,
49   $getRoot,
50   $hasAncestor,
51   $isTokenOrSegmented,
52   $setCompositionKey,
53   doesContainGrapheme,
54   getDOMSelection,
55   getDOMTextNode,
56   getElementByKeyOrThrow,
57   getTextNodeOffset,
58   INTERNAL_$isBlock,
59   isSelectionCapturedInDecoratorInput,
60   isSelectionWithinEditor,
61   removeDOMBlockCursorElement,
62   scrollIntoViewIfNeeded,
63   toggleTextFormatType,
64 } from './LexicalUtils';
65 import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
66
67 export type TextPointType = {
68   _selection: BaseSelection;
69   getNode: () => TextNode;
70   is: (point: PointType) => boolean;
71   isBefore: (point: PointType) => boolean;
72   key: NodeKey;
73   offset: number;
74   set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
75   type: 'text';
76 };
77
78 export type ElementPointType = {
79   _selection: BaseSelection;
80   getNode: () => ElementNode;
81   is: (point: PointType) => boolean;
82   isBefore: (point: PointType) => boolean;
83   key: NodeKey;
84   offset: number;
85   set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
86   type: 'element';
87 };
88
89 export type PointType = TextPointType | ElementPointType;
90
91 export class Point {
92   key: NodeKey;
93   offset: number;
94   type: 'text' | 'element';
95   _selection: BaseSelection | null;
96
97   constructor(key: NodeKey, offset: number, type: 'text' | 'element') {
98     this._selection = null;
99     this.key = key;
100     this.offset = offset;
101     this.type = type;
102   }
103
104   is(point: PointType): boolean {
105     return (
106       this.key === point.key &&
107       this.offset === point.offset &&
108       this.type === point.type
109     );
110   }
111
112   isBefore(b: PointType): boolean {
113     let aNode = this.getNode();
114     let bNode = b.getNode();
115     const aOffset = this.offset;
116     const bOffset = b.offset;
117
118     if ($isElementNode(aNode)) {
119       const aNodeDescendant = aNode.getDescendantByIndex<ElementNode>(aOffset);
120       aNode = aNodeDescendant != null ? aNodeDescendant : aNode;
121     }
122     if ($isElementNode(bNode)) {
123       const bNodeDescendant = bNode.getDescendantByIndex<ElementNode>(bOffset);
124       bNode = bNodeDescendant != null ? bNodeDescendant : bNode;
125     }
126     if (aNode === bNode) {
127       return aOffset < bOffset;
128     }
129     return aNode.isBefore(bNode);
130   }
131
132   getNode(): LexicalNode {
133     const key = this.key;
134     const node = $getNodeByKey(key);
135     if (node === null) {
136       invariant(false, 'Point.getNode: node not found');
137     }
138     return node;
139   }
140
141   set(key: NodeKey, offset: number, type: 'text' | 'element'): void {
142     const selection = this._selection;
143     const oldKey = this.key;
144     this.key = key;
145     this.offset = offset;
146     this.type = type;
147     if (!isCurrentlyReadOnlyMode()) {
148       if ($getCompositionKey() === oldKey) {
149         $setCompositionKey(key);
150       }
151       if (selection !== null) {
152         selection.setCachedNodes(null);
153         selection.dirty = true;
154       }
155     }
156   }
157 }
158
159 export function $createPoint(
160   key: NodeKey,
161   offset: number,
162   type: 'text' | 'element',
163 ): PointType {
164   // @ts-expect-error: intentionally cast as we use a class for perf reasons
165   return new Point(key, offset, type);
166 }
167
168 function selectPointOnNode(point: PointType, node: LexicalNode): void {
169   let key = node.__key;
170   let offset = point.offset;
171   let type: 'element' | 'text' = 'element';
172   if ($isTextNode(node)) {
173     type = 'text';
174     const textContentLength = node.getTextContentSize();
175     if (offset > textContentLength) {
176       offset = textContentLength;
177     }
178   } else if (!$isElementNode(node)) {
179     const nextSibling = node.getNextSibling();
180     if ($isTextNode(nextSibling)) {
181       key = nextSibling.__key;
182       offset = 0;
183       type = 'text';
184     } else {
185       const parentNode = node.getParent();
186       if (parentNode) {
187         key = parentNode.__key;
188         offset = node.getIndexWithinParent() + 1;
189       }
190     }
191   }
192   point.set(key, offset, type);
193 }
194
195 export function $moveSelectionPointToEnd(
196   point: PointType,
197   node: LexicalNode,
198 ): void {
199   if ($isElementNode(node)) {
200     const lastNode = node.getLastDescendant();
201     if ($isElementNode(lastNode) || $isTextNode(lastNode)) {
202       selectPointOnNode(point, lastNode);
203     } else {
204       selectPointOnNode(point, node);
205     }
206   } else {
207     selectPointOnNode(point, node);
208   }
209 }
210
211 function $transferStartingElementPointToTextPoint(
212   start: ElementPointType,
213   end: PointType,
214   format: number,
215   style: string,
216 ): void {
217   const element = start.getNode();
218   const placementNode = element.getChildAtIndex(start.offset);
219   const textNode = $createTextNode();
220   const target = $isRootNode(element)
221     ? $createParagraphNode().append(textNode)
222     : textNode;
223   textNode.setFormat(format);
224   textNode.setStyle(style);
225   if (placementNode === null) {
226     element.append(target);
227   } else {
228     placementNode.insertBefore(target);
229   }
230   // Transfer the element point to a text point.
231   if (start.is(end)) {
232     end.set(textNode.__key, 0, 'text');
233   }
234   start.set(textNode.__key, 0, 'text');
235 }
236
237 function $setPointValues(
238   point: PointType,
239   key: NodeKey,
240   offset: number,
241   type: 'text' | 'element',
242 ): void {
243   point.key = key;
244   point.offset = offset;
245   point.type = type;
246 }
247
248 export interface BaseSelection {
249   _cachedNodes: Array<LexicalNode> | null;
250   dirty: boolean;
251
252   clone(): BaseSelection;
253   extract(): Array<LexicalNode>;
254   getNodes(): Array<LexicalNode>;
255   getTextContent(): string;
256   insertText(text: string): void;
257   insertRawText(text: string): void;
258   is(selection: null | BaseSelection): boolean;
259   insertNodes(nodes: Array<LexicalNode>): void;
260   getStartEndPoints(): null | [PointType, PointType];
261   isCollapsed(): boolean;
262   isBackward(): boolean;
263   getCachedNodes(): LexicalNode[] | null;
264   setCachedNodes(nodes: LexicalNode[] | null): void;
265 }
266
267 export class NodeSelection implements BaseSelection {
268   _nodes: Set<NodeKey>;
269   _cachedNodes: Array<LexicalNode> | null;
270   dirty: boolean;
271
272   constructor(objects: Set<NodeKey>) {
273     this._cachedNodes = null;
274     this._nodes = objects;
275     this.dirty = false;
276   }
277
278   getCachedNodes(): LexicalNode[] | null {
279     return this._cachedNodes;
280   }
281
282   setCachedNodes(nodes: LexicalNode[] | null): void {
283     this._cachedNodes = nodes;
284   }
285
286   is(selection: null | BaseSelection): boolean {
287     if (!$isNodeSelection(selection)) {
288       return false;
289     }
290     const a: Set<NodeKey> = this._nodes;
291     const b: Set<NodeKey> = selection._nodes;
292     return a.size === b.size && Array.from(a).every((key) => b.has(key));
293   }
294
295   isCollapsed(): boolean {
296     return false;
297   }
298
299   isBackward(): boolean {
300     return false;
301   }
302
303   getStartEndPoints(): null {
304     return null;
305   }
306
307   add(key: NodeKey): void {
308     this.dirty = true;
309     this._nodes.add(key);
310     this._cachedNodes = null;
311   }
312
313   delete(key: NodeKey): void {
314     this.dirty = true;
315     this._nodes.delete(key);
316     this._cachedNodes = null;
317   }
318
319   clear(): void {
320     this.dirty = true;
321     this._nodes.clear();
322     this._cachedNodes = null;
323   }
324
325   has(key: NodeKey): boolean {
326     return this._nodes.has(key);
327   }
328
329   clone(): NodeSelection {
330     return new NodeSelection(new Set(this._nodes));
331   }
332
333   extract(): Array<LexicalNode> {
334     return this.getNodes();
335   }
336
337   insertRawText(text: string): void {
338     // Do nothing?
339   }
340
341   insertText(): void {
342     // Do nothing?
343   }
344
345   insertNodes(nodes: Array<LexicalNode>) {
346     const selectedNodes = this.getNodes();
347     const selectedNodesLength = selectedNodes.length;
348     const lastSelectedNode = selectedNodes[selectedNodesLength - 1];
349     let selectionAtEnd: RangeSelection;
350     // Insert nodes
351     if ($isTextNode(lastSelectedNode)) {
352       selectionAtEnd = lastSelectedNode.select();
353     } else {
354       const index = lastSelectedNode.getIndexWithinParent() + 1;
355       selectionAtEnd = lastSelectedNode.getParentOrThrow().select(index, index);
356     }
357     selectionAtEnd.insertNodes(nodes);
358     // Remove selected nodes
359     for (let i = 0; i < selectedNodesLength; i++) {
360       selectedNodes[i].remove();
361     }
362   }
363
364   getNodes(): Array<LexicalNode> {
365     const cachedNodes = this._cachedNodes;
366     if (cachedNodes !== null) {
367       return cachedNodes;
368     }
369     const objects = this._nodes;
370     const nodes = [];
371     for (const object of objects) {
372       const node = $getNodeByKey(object);
373       if (node !== null) {
374         nodes.push(node);
375       }
376     }
377     if (!isCurrentlyReadOnlyMode()) {
378       this._cachedNodes = nodes;
379     }
380     return nodes;
381   }
382
383   getTextContent(): string {
384     const nodes = this.getNodes();
385     let textContent = '';
386     for (let i = 0; i < nodes.length; i++) {
387       textContent += nodes[i].getTextContent();
388     }
389     return textContent;
390   }
391 }
392
393 export function $isRangeSelection(x: unknown): x is RangeSelection {
394   return x instanceof RangeSelection;
395 }
396
397 export class RangeSelection implements BaseSelection {
398   format: number;
399   style: string;
400   anchor: PointType;
401   focus: PointType;
402   _cachedNodes: Array<LexicalNode> | null;
403   dirty: boolean;
404
405   constructor(
406     anchor: PointType,
407     focus: PointType,
408     format: number,
409     style: string,
410   ) {
411     this.anchor = anchor;
412     this.focus = focus;
413     anchor._selection = this;
414     focus._selection = this;
415     this._cachedNodes = null;
416     this.format = format;
417     this.style = style;
418     this.dirty = false;
419   }
420
421   getCachedNodes(): LexicalNode[] | null {
422     return this._cachedNodes;
423   }
424
425   setCachedNodes(nodes: LexicalNode[] | null): void {
426     this._cachedNodes = nodes;
427   }
428
429   /**
430    * Used to check if the provided selections is equal to this one by value,
431    * inluding anchor, focus, format, and style properties.
432    * @param selection - the Selection to compare this one to.
433    * @returns true if the Selections are equal, false otherwise.
434    */
435   is(selection: null | BaseSelection): boolean {
436     if (!$isRangeSelection(selection)) {
437       return false;
438     }
439     return (
440       this.anchor.is(selection.anchor) &&
441       this.focus.is(selection.focus) &&
442       this.format === selection.format &&
443       this.style === selection.style
444     );
445   }
446
447   /**
448    * Returns whether the Selection is "collapsed", meaning the anchor and focus are
449    * the same node and have the same offset.
450    *
451    * @returns true if the Selection is collapsed, false otherwise.
452    */
453   isCollapsed(): boolean {
454     return this.anchor.is(this.focus);
455   }
456
457   /**
458    * Gets all the nodes in the Selection. Uses caching to make it generally suitable
459    * for use in hot paths.
460    *
461    * @returns an Array containing all the nodes in the Selection
462    */
463   getNodes(): Array<LexicalNode> {
464     const cachedNodes = this._cachedNodes;
465     if (cachedNodes !== null) {
466       return cachedNodes;
467     }
468     const anchor = this.anchor;
469     const focus = this.focus;
470     const isBefore = anchor.isBefore(focus);
471     const firstPoint = isBefore ? anchor : focus;
472     const lastPoint = isBefore ? focus : anchor;
473     let firstNode = firstPoint.getNode();
474     let lastNode = lastPoint.getNode();
475     const startOffset = firstPoint.offset;
476     const endOffset = lastPoint.offset;
477
478     if ($isElementNode(firstNode)) {
479       const firstNodeDescendant =
480         firstNode.getDescendantByIndex<ElementNode>(startOffset);
481       firstNode = firstNodeDescendant != null ? firstNodeDescendant : firstNode;
482     }
483     if ($isElementNode(lastNode)) {
484       let lastNodeDescendant =
485         lastNode.getDescendantByIndex<ElementNode>(endOffset);
486       // We don't want to over-select, as node selection infers the child before
487       // the last descendant, not including that descendant.
488       if (
489         lastNodeDescendant !== null &&
490         lastNodeDescendant !== firstNode &&
491         lastNode.getChildAtIndex(endOffset) === lastNodeDescendant
492       ) {
493         lastNodeDescendant = lastNodeDescendant.getPreviousSibling();
494       }
495       lastNode = lastNodeDescendant != null ? lastNodeDescendant : lastNode;
496     }
497
498     let nodes: Array<LexicalNode>;
499
500     if (firstNode.is(lastNode)) {
501       if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0) {
502         nodes = [];
503       } else {
504         nodes = [firstNode];
505       }
506     } else {
507       nodes = firstNode.getNodesBetween(lastNode);
508     }
509     if (!isCurrentlyReadOnlyMode()) {
510       this._cachedNodes = nodes;
511     }
512     return nodes;
513   }
514
515   /**
516    * Sets this Selection to be of type "text" at the provided anchor and focus values.
517    *
518    * @param anchorNode - the anchor node to set on the Selection
519    * @param anchorOffset - the offset to set on the Selection
520    * @param focusNode - the focus node to set on the Selection
521    * @param focusOffset - the focus offset to set on the Selection
522    */
523   setTextNodeRange(
524     anchorNode: TextNode,
525     anchorOffset: number,
526     focusNode: TextNode,
527     focusOffset: number,
528   ): void {
529     $setPointValues(this.anchor, anchorNode.__key, anchorOffset, 'text');
530     $setPointValues(this.focus, focusNode.__key, focusOffset, 'text');
531     this._cachedNodes = null;
532     this.dirty = true;
533   }
534
535   /**
536    * Gets the (plain) text content of all the nodes in the selection.
537    *
538    * @returns a string representing the text content of all the nodes in the Selection
539    */
540   getTextContent(): string {
541     const nodes = this.getNodes();
542     if (nodes.length === 0) {
543       return '';
544     }
545     const firstNode = nodes[0];
546     const lastNode = nodes[nodes.length - 1];
547     const anchor = this.anchor;
548     const focus = this.focus;
549     const isBefore = anchor.isBefore(focus);
550     const [anchorOffset, focusOffset] = $getCharacterOffsets(this);
551     let textContent = '';
552     let prevWasElement = true;
553     for (let i = 0; i < nodes.length; i++) {
554       const node = nodes[i];
555       if ($isElementNode(node) && !node.isInline()) {
556         if (!prevWasElement) {
557           textContent += '\n';
558         }
559         if (node.isEmpty()) {
560           prevWasElement = false;
561         } else {
562           prevWasElement = true;
563         }
564       } else {
565         prevWasElement = false;
566         if ($isTextNode(node)) {
567           let text = node.getTextContent();
568           if (node === firstNode) {
569             if (node === lastNode) {
570               if (
571                 anchor.type !== 'element' ||
572                 focus.type !== 'element' ||
573                 focus.offset === anchor.offset
574               ) {
575                 text =
576                   anchorOffset < focusOffset
577                     ? text.slice(anchorOffset, focusOffset)
578                     : text.slice(focusOffset, anchorOffset);
579               }
580             } else {
581               text = isBefore
582                 ? text.slice(anchorOffset)
583                 : text.slice(focusOffset);
584             }
585           } else if (node === lastNode) {
586             text = isBefore
587               ? text.slice(0, focusOffset)
588               : text.slice(0, anchorOffset);
589           }
590           textContent += text;
591         } else if (
592           ($isDecoratorNode(node) || $isLineBreakNode(node)) &&
593           (node !== lastNode || !this.isCollapsed())
594         ) {
595           textContent += node.getTextContent();
596         }
597       }
598     }
599     return textContent;
600   }
601
602   /**
603    * Attempts to map a DOM selection range onto this Lexical Selection,
604    * setting the anchor, focus, and type accordingly
605    *
606    * @param range a DOM Selection range conforming to the StaticRange interface.
607    */
608   applyDOMRange(range: StaticRange): void {
609     const editor = getActiveEditor();
610     const currentEditorState = editor.getEditorState();
611     const lastSelection = currentEditorState._selection;
612     const resolvedSelectionPoints = $internalResolveSelectionPoints(
613       range.startContainer,
614       range.startOffset,
615       range.endContainer,
616       range.endOffset,
617       editor,
618       lastSelection,
619     );
620     if (resolvedSelectionPoints === null) {
621       return;
622     }
623     const [anchorPoint, focusPoint] = resolvedSelectionPoints;
624     $setPointValues(
625       this.anchor,
626       anchorPoint.key,
627       anchorPoint.offset,
628       anchorPoint.type,
629     );
630     $setPointValues(
631       this.focus,
632       focusPoint.key,
633       focusPoint.offset,
634       focusPoint.type,
635     );
636     this._cachedNodes = null;
637   }
638
639   /**
640    * Creates a new RangeSelection, copying over all the property values from this one.
641    *
642    * @returns a new RangeSelection with the same property values as this one.
643    */
644   clone(): RangeSelection {
645     const anchor = this.anchor;
646     const focus = this.focus;
647     const selection = new RangeSelection(
648       $createPoint(anchor.key, anchor.offset, anchor.type),
649       $createPoint(focus.key, focus.offset, focus.type),
650       this.format,
651       this.style,
652     );
653     return selection;
654   }
655
656   /**
657    * Toggles the provided format on all the TextNodes in the Selection.
658    *
659    * @param format a string TextFormatType to toggle on the TextNodes in the selection
660    */
661   toggleFormat(format: TextFormatType): void {
662     this.format = toggleTextFormatType(this.format, format, null);
663     this.dirty = true;
664   }
665
666   /**
667    * Sets the value of the style property on the Selection
668    *
669    * @param style - the style to set at the value of the style property.
670    */
671   setStyle(style: string): void {
672     this.style = style;
673     this.dirty = true;
674   }
675
676   /**
677    * Returns whether the provided TextFormatType is present on the Selection. This will be true if any node in the Selection
678    * has the specified format.
679    *
680    * @param type the TextFormatType to check for.
681    * @returns true if the provided format is currently toggled on on the Selection, false otherwise.
682    */
683   hasFormat(type: TextFormatType): boolean {
684     const formatFlag = TEXT_TYPE_TO_FORMAT[type];
685     return (this.format & formatFlag) !== 0;
686   }
687
688   /**
689    * Attempts to insert the provided text into the EditorState at the current Selection.
690    * converts tabs, newlines, and carriage returns into LexicalNodes.
691    *
692    * @param text the text to insert into the Selection
693    */
694   insertRawText(text: string): void {
695     const parts = text.split(/(\r?\n|\t)/);
696     const nodes = [];
697     const length = parts.length;
698     for (let i = 0; i < length; i++) {
699       const part = parts[i];
700       if (part === '\n' || part === '\r\n') {
701         nodes.push($createLineBreakNode());
702       } else if (part === '\t') {
703         nodes.push($createTabNode());
704       } else {
705         nodes.push($createTextNode(part));
706       }
707     }
708     this.insertNodes(nodes);
709   }
710
711   /**
712    * Attempts to insert the provided text into the EditorState at the current Selection as a new
713    * Lexical TextNode, according to a series of insertion heuristics based on the selection type and position.
714    *
715    * @param text the text to insert into the Selection
716    */
717   insertText(text: string): void {
718     const anchor = this.anchor;
719     const focus = this.focus;
720     const format = this.format;
721     const style = this.style;
722     let firstPoint = anchor;
723     let endPoint = focus;
724     if (!this.isCollapsed() && focus.isBefore(anchor)) {
725       firstPoint = focus;
726       endPoint = anchor;
727     }
728     if (firstPoint.type === 'element') {
729       $transferStartingElementPointToTextPoint(
730         firstPoint,
731         endPoint,
732         format,
733         style,
734       );
735     }
736     const startOffset = firstPoint.offset;
737     let endOffset = endPoint.offset;
738     const selectedNodes = this.getNodes();
739     const selectedNodesLength = selectedNodes.length;
740     let firstNode: TextNode = selectedNodes[0] as TextNode;
741
742     if (!$isTextNode(firstNode)) {
743       invariant(false, 'insertText: first node is not a text node');
744     }
745     const firstNodeText = firstNode.getTextContent();
746     const firstNodeTextLength = firstNodeText.length;
747     const firstNodeParent = firstNode.getParentOrThrow();
748     const lastIndex = selectedNodesLength - 1;
749     let lastNode = selectedNodes[lastIndex];
750
751     if (selectedNodesLength === 1 && endPoint.type === 'element') {
752       endOffset = firstNodeTextLength;
753       endPoint.set(firstPoint.key, endOffset, 'text');
754     }
755
756     if (
757       this.isCollapsed() &&
758       startOffset === firstNodeTextLength &&
759       (firstNode.isSegmented() ||
760         firstNode.isToken() ||
761         !firstNode.canInsertTextAfter() ||
762         (!firstNodeParent.canInsertTextAfter() &&
763           firstNode.getNextSibling() === null))
764     ) {
765       let nextSibling = firstNode.getNextSibling<TextNode>();
766       if (
767         !$isTextNode(nextSibling) ||
768         !nextSibling.canInsertTextBefore() ||
769         $isTokenOrSegmented(nextSibling)
770       ) {
771         nextSibling = $createTextNode();
772         nextSibling.setFormat(format);
773         nextSibling.setStyle(style);
774         if (!firstNodeParent.canInsertTextAfter()) {
775           firstNodeParent.insertAfter(nextSibling);
776         } else {
777           firstNode.insertAfter(nextSibling);
778         }
779       }
780       nextSibling.select(0, 0);
781       firstNode = nextSibling;
782       if (text !== '') {
783         this.insertText(text);
784         return;
785       }
786     } else if (
787       this.isCollapsed() &&
788       startOffset === 0 &&
789       (firstNode.isSegmented() ||
790         firstNode.isToken() ||
791         !firstNode.canInsertTextBefore() ||
792         (!firstNodeParent.canInsertTextBefore() &&
793           firstNode.getPreviousSibling() === null))
794     ) {
795       let prevSibling = firstNode.getPreviousSibling<TextNode>();
796       if (!$isTextNode(prevSibling) || $isTokenOrSegmented(prevSibling)) {
797         prevSibling = $createTextNode();
798         prevSibling.setFormat(format);
799         if (!firstNodeParent.canInsertTextBefore()) {
800           firstNodeParent.insertBefore(prevSibling);
801         } else {
802           firstNode.insertBefore(prevSibling);
803         }
804       }
805       prevSibling.select();
806       firstNode = prevSibling;
807       if (text !== '') {
808         this.insertText(text);
809         return;
810       }
811     } else if (firstNode.isSegmented() && startOffset !== firstNodeTextLength) {
812       const textNode = $createTextNode(firstNode.getTextContent());
813       textNode.setFormat(format);
814       firstNode.replace(textNode);
815       firstNode = textNode;
816     } else if (!this.isCollapsed() && text !== '') {
817       // When the firstNode or lastNode parents are elements that
818       // do not allow text to be inserted before or after, we first
819       // clear the content. Then we normalize selection, then insert
820       // the new content.
821       const lastNodeParent = lastNode.getParent();
822
823       if (
824         !firstNodeParent.canInsertTextBefore() ||
825         !firstNodeParent.canInsertTextAfter() ||
826         ($isElementNode(lastNodeParent) &&
827           (!lastNodeParent.canInsertTextBefore() ||
828             !lastNodeParent.canInsertTextAfter()))
829       ) {
830         this.insertText('');
831         $normalizeSelectionPointsForBoundaries(this.anchor, this.focus, null);
832         this.insertText(text);
833         return;
834       }
835     }
836
837     if (selectedNodesLength === 1) {
838       if (firstNode.isToken()) {
839         const textNode = $createTextNode(text);
840         textNode.select();
841         firstNode.replace(textNode);
842         return;
843       }
844       const firstNodeFormat = firstNode.getFormat();
845       const firstNodeStyle = firstNode.getStyle();
846
847       if (
848         startOffset === endOffset &&
849         (firstNodeFormat !== format || firstNodeStyle !== style)
850       ) {
851         if (firstNode.getTextContent() === '') {
852           firstNode.setFormat(format);
853           firstNode.setStyle(style);
854         } else {
855           const textNode = $createTextNode(text);
856           textNode.setFormat(format);
857           textNode.setStyle(style);
858           textNode.select();
859           if (startOffset === 0) {
860             firstNode.insertBefore(textNode, false);
861           } else {
862             const [targetNode] = firstNode.splitText(startOffset);
863             targetNode.insertAfter(textNode, false);
864           }
865           // When composing, we need to adjust the anchor offset so that
866           // we correctly replace that right range.
867           if (textNode.isComposing() && this.anchor.type === 'text') {
868             this.anchor.offset -= text.length;
869           }
870           return;
871         }
872       } else if ($isTabNode(firstNode)) {
873         // We don't need to check for delCount because there is only the entire selected node case
874         // that can hit here for content size 1 and with canInsertTextBeforeAfter false
875         const textNode = $createTextNode(text);
876         textNode.setFormat(format);
877         textNode.setStyle(style);
878         textNode.select();
879         firstNode.replace(textNode);
880         return;
881       }
882       const delCount = endOffset - startOffset;
883
884       firstNode = firstNode.spliceText(startOffset, delCount, text, true);
885       if (firstNode.getTextContent() === '') {
886         firstNode.remove();
887       } else if (this.anchor.type === 'text') {
888         if (firstNode.isComposing()) {
889           // When composing, we need to adjust the anchor offset so that
890           // we correctly replace that right range.
891           this.anchor.offset -= text.length;
892         } else {
893           this.format = firstNodeFormat;
894           this.style = firstNodeStyle;
895         }
896       }
897     } else {
898       const markedNodeKeysForKeep = new Set([
899         ...firstNode.getParentKeys(),
900         ...lastNode.getParentKeys(),
901       ]);
902
903       // We have to get the parent elements before the next section,
904       // as in that section we might mutate the lastNode.
905       const firstElement = $isElementNode(firstNode)
906         ? firstNode
907         : firstNode.getParentOrThrow();
908       let lastElement = $isElementNode(lastNode)
909         ? lastNode
910         : lastNode.getParentOrThrow();
911       let lastElementChild = lastNode;
912
913       // If the last element is inline, we should instead look at getting
914       // the nodes of its parent, rather than itself. This behavior will
915       // then better match how text node insertions work. We will need to
916       // also update the last element's child accordingly as we do this.
917       if (!firstElement.is(lastElement) && lastElement.isInline()) {
918         // Keep traversing till we have a non-inline element parent.
919         do {
920           lastElementChild = lastElement;
921           lastElement = lastElement.getParentOrThrow();
922         } while (lastElement.isInline());
923       }
924
925       // Handle mutations to the last node.
926       if (
927         (endPoint.type === 'text' &&
928           (endOffset !== 0 || lastNode.getTextContent() === '')) ||
929         (endPoint.type === 'element' &&
930           lastNode.getIndexWithinParent() < endOffset)
931       ) {
932         if (
933           $isTextNode(lastNode) &&
934           !lastNode.isToken() &&
935           endOffset !== lastNode.getTextContentSize()
936         ) {
937           if (lastNode.isSegmented()) {
938             const textNode = $createTextNode(lastNode.getTextContent());
939             lastNode.replace(textNode);
940             lastNode = textNode;
941           }
942           // root node selections only select whole nodes, so no text splice is necessary
943           if (!$isRootNode(endPoint.getNode()) && endPoint.type === 'text') {
944             lastNode = (lastNode as TextNode).spliceText(0, endOffset, '');
945           }
946           markedNodeKeysForKeep.add(lastNode.__key);
947         } else {
948           const lastNodeParent = lastNode.getParentOrThrow();
949           if (
950             !lastNodeParent.canBeEmpty() &&
951             lastNodeParent.getChildrenSize() === 1
952           ) {
953             lastNodeParent.remove();
954           } else {
955             lastNode.remove();
956           }
957         }
958       } else {
959         markedNodeKeysForKeep.add(lastNode.__key);
960       }
961
962       // Either move the remaining nodes of the last parent to after
963       // the first child, or remove them entirely. If the last parent
964       // is the same as the first parent, this logic also works.
965       const lastNodeChildren = lastElement.getChildren();
966       const selectedNodesSet = new Set(selectedNodes);
967       const firstAndLastElementsAreEqual = firstElement.is(lastElement);
968
969       // We choose a target to insert all nodes after. In the case of having
970       // and inline starting parent element with a starting node that has no
971       // siblings, we should insert after the starting parent element, otherwise
972       // we will incorrectly merge into the starting parent element.
973       // TODO: should we keep on traversing parents if we're inside another
974       // nested inline element?
975       const insertionTarget =
976         firstElement.isInline() && firstNode.getNextSibling() === null
977           ? firstElement
978           : firstNode;
979
980       for (let i = lastNodeChildren.length - 1; i >= 0; i--) {
981         const lastNodeChild = lastNodeChildren[i];
982
983         if (
984           lastNodeChild.is(firstNode) ||
985           ($isElementNode(lastNodeChild) && lastNodeChild.isParentOf(firstNode))
986         ) {
987           break;
988         }
989
990         if (lastNodeChild.isAttached()) {
991           if (
992             !selectedNodesSet.has(lastNodeChild) ||
993             lastNodeChild.is(lastElementChild)
994           ) {
995             if (!firstAndLastElementsAreEqual) {
996               insertionTarget.insertAfter(lastNodeChild, false);
997             }
998           } else {
999             lastNodeChild.remove();
1000           }
1001         }
1002       }
1003
1004       if (!firstAndLastElementsAreEqual) {
1005         // Check if we have already moved out all the nodes of the
1006         // last parent, and if so, traverse the parent tree and mark
1007         // them all as being able to deleted too.
1008         let parent: ElementNode | null = lastElement;
1009         let lastRemovedParent = null;
1010
1011         while (parent !== null) {
1012           const children = parent.getChildren();
1013           const childrenLength = children.length;
1014           if (
1015             childrenLength === 0 ||
1016             children[childrenLength - 1].is(lastRemovedParent)
1017           ) {
1018             markedNodeKeysForKeep.delete(parent.__key);
1019             lastRemovedParent = parent;
1020           }
1021           parent = parent.getParent();
1022         }
1023       }
1024
1025       // Ensure we do splicing after moving of nodes, as splicing
1026       // can have side-effects (in the case of hashtags).
1027       if (!firstNode.isToken()) {
1028         firstNode = firstNode.spliceText(
1029           startOffset,
1030           firstNodeTextLength - startOffset,
1031           text,
1032           true,
1033         );
1034         if (firstNode.getTextContent() === '') {
1035           firstNode.remove();
1036         } else if (firstNode.isComposing() && this.anchor.type === 'text') {
1037           // When composing, we need to adjust the anchor offset so that
1038           // we correctly replace that right range.
1039           this.anchor.offset -= text.length;
1040         }
1041       } else if (startOffset === firstNodeTextLength) {
1042         firstNode.select();
1043       } else {
1044         const textNode = $createTextNode(text);
1045         textNode.select();
1046         firstNode.replace(textNode);
1047       }
1048
1049       // Remove all selected nodes that haven't already been removed.
1050       for (let i = 1; i < selectedNodesLength; i++) {
1051         const selectedNode = selectedNodes[i];
1052         const key = selectedNode.__key;
1053         if (!markedNodeKeysForKeep.has(key)) {
1054           selectedNode.remove();
1055         }
1056       }
1057     }
1058   }
1059
1060   /**
1061    * Removes the text in the Selection, adjusting the EditorState accordingly.
1062    */
1063   removeText(): void {
1064     this.insertText('');
1065   }
1066
1067   /**
1068    * Applies the provided format to the TextNodes in the Selection, splitting or
1069    * merging nodes as necessary.
1070    *
1071    * @param formatType the format type to apply to the nodes in the Selection.
1072    */
1073   formatText(formatType: TextFormatType): void {
1074     if (this.isCollapsed()) {
1075       this.toggleFormat(formatType);
1076       // When changing format, we should stop composition
1077       $setCompositionKey(null);
1078       return;
1079     }
1080
1081     const selectedNodes = this.getNodes();
1082     const selectedTextNodes: Array<TextNode> = [];
1083     for (const selectedNode of selectedNodes) {
1084       if ($isTextNode(selectedNode)) {
1085         selectedTextNodes.push(selectedNode);
1086       }
1087     }
1088
1089     const selectedTextNodesLength = selectedTextNodes.length;
1090     if (selectedTextNodesLength === 0) {
1091       this.toggleFormat(formatType);
1092       // When changing format, we should stop composition
1093       $setCompositionKey(null);
1094       return;
1095     }
1096
1097     const anchor = this.anchor;
1098     const focus = this.focus;
1099     const isBackward = this.isBackward();
1100     const startPoint = isBackward ? focus : anchor;
1101     const endPoint = isBackward ? anchor : focus;
1102
1103     let firstIndex = 0;
1104     let firstNode = selectedTextNodes[0];
1105     let startOffset = startPoint.type === 'element' ? 0 : startPoint.offset;
1106
1107     // In case selection started at the end of text node use next text node
1108     if (
1109       startPoint.type === 'text' &&
1110       startOffset === firstNode.getTextContentSize()
1111     ) {
1112       firstIndex = 1;
1113       firstNode = selectedTextNodes[1];
1114       startOffset = 0;
1115     }
1116
1117     if (firstNode == null) {
1118       return;
1119     }
1120
1121     const firstNextFormat = firstNode.getFormatFlags(formatType, null);
1122
1123     const lastIndex = selectedTextNodesLength - 1;
1124     let lastNode = selectedTextNodes[lastIndex];
1125     const endOffset =
1126       endPoint.type === 'text'
1127         ? endPoint.offset
1128         : lastNode.getTextContentSize();
1129
1130     // Single node selected
1131     if (firstNode.is(lastNode)) {
1132       // No actual text is selected, so do nothing.
1133       if (startOffset === endOffset) {
1134         return;
1135       }
1136       // The entire node is selected or it is token, so just format it
1137       if (
1138         $isTokenOrSegmented(firstNode) ||
1139         (startOffset === 0 && endOffset === firstNode.getTextContentSize())
1140       ) {
1141         firstNode.setFormat(firstNextFormat);
1142       } else {
1143         // Node is partially selected, so split it into two nodes
1144         // add style the selected one.
1145         const splitNodes = firstNode.splitText(startOffset, endOffset);
1146         const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
1147         replacement.setFormat(firstNextFormat);
1148
1149         // Update selection only if starts/ends on text node
1150         if (startPoint.type === 'text') {
1151           startPoint.set(replacement.__key, 0, 'text');
1152         }
1153         if (endPoint.type === 'text') {
1154           endPoint.set(replacement.__key, endOffset - startOffset, 'text');
1155         }
1156       }
1157
1158       this.format = firstNextFormat;
1159
1160       return;
1161     }
1162     // Multiple nodes selected
1163     // The entire first node isn't selected, so split it
1164     if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) {
1165       [, firstNode as TextNode] = firstNode.splitText(startOffset);
1166       startOffset = 0;
1167     }
1168     firstNode.setFormat(firstNextFormat);
1169
1170     const lastNextFormat = lastNode.getFormatFlags(formatType, firstNextFormat);
1171     // If the offset is 0, it means no actual characters are selected,
1172     // so we skip formatting the last node altogether.
1173     if (endOffset > 0) {
1174       if (
1175         endOffset !== lastNode.getTextContentSize() &&
1176         !$isTokenOrSegmented(lastNode)
1177       ) {
1178         [lastNode as TextNode] = lastNode.splitText(endOffset);
1179       }
1180       lastNode.setFormat(lastNextFormat);
1181     }
1182
1183     // Process all text nodes in between
1184     for (let i = firstIndex + 1; i < lastIndex; i++) {
1185       const textNode = selectedTextNodes[i];
1186       const nextFormat = textNode.getFormatFlags(formatType, lastNextFormat);
1187       textNode.setFormat(nextFormat);
1188     }
1189
1190     // Update selection only if starts/ends on text node
1191     if (startPoint.type === 'text') {
1192       startPoint.set(firstNode.__key, startOffset, 'text');
1193     }
1194     if (endPoint.type === 'text') {
1195       endPoint.set(lastNode.__key, endOffset, 'text');
1196     }
1197
1198     this.format = firstNextFormat | lastNextFormat;
1199   }
1200
1201   /**
1202    * Attempts to "intelligently" insert an arbitrary list of Lexical nodes into the EditorState at the
1203    * current Selection according to a set of heuristics that determine how surrounding nodes
1204    * should be changed, replaced, or moved to accomodate the incoming ones.
1205    *
1206    * @param nodes - the nodes to insert
1207    */
1208   insertNodes(nodes: Array<LexicalNode>): void {
1209     if (nodes.length === 0) {
1210       return;
1211     }
1212     if (this.anchor.key === 'root') {
1213       this.insertParagraph();
1214       const selection = $getSelection();
1215       invariant(
1216         $isRangeSelection(selection),
1217         'Expected RangeSelection after insertParagraph',
1218       );
1219       return selection.insertNodes(nodes);
1220     }
1221
1222     const firstPoint = this.isBackward() ? this.focus : this.anchor;
1223     const firstBlock = $getAncestor(firstPoint.getNode(), INTERNAL_$isBlock)!;
1224
1225     const last = nodes[nodes.length - 1]!;
1226
1227     // CASE 1: insert inside a code block
1228     if ('__language' in firstBlock && $isElementNode(firstBlock)) {
1229       if ('__language' in nodes[0]) {
1230         this.insertText(nodes[0].getTextContent());
1231       } else {
1232         const index = $removeTextAndSplitBlock(this);
1233         firstBlock.splice(index, 0, nodes);
1234         last.selectEnd();
1235       }
1236       return;
1237     }
1238
1239     // CASE 2: All elements of the array are inline
1240     const notInline = (node: LexicalNode) =>
1241       ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline();
1242
1243     if (!nodes.some(notInline)) {
1244       invariant(
1245         $isElementNode(firstBlock),
1246         "Expected 'firstBlock' to be an ElementNode",
1247       );
1248       const index = $removeTextAndSplitBlock(this);
1249       firstBlock.splice(index, 0, nodes);
1250       last.selectEnd();
1251       return;
1252     }
1253
1254     // CASE 3: At least 1 element of the array is not inline
1255     const blocksParent = $wrapInlineNodes(nodes);
1256     const nodeToSelect = blocksParent.getLastDescendant()!;
1257     const blocks = blocksParent.getChildren();
1258     const isMergeable = (node: LexicalNode): node is ElementNode =>
1259       $isElementNode(node) &&
1260       INTERNAL_$isBlock(node) &&
1261       !node.isEmpty() &&
1262       $isElementNode(firstBlock) &&
1263       (!firstBlock.isEmpty() || firstBlock.canMergeWhenEmpty());
1264
1265     const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty();
1266     const insertedParagraph = shouldInsert ? this.insertParagraph() : null;
1267     const lastToInsert = blocks[blocks.length - 1];
1268     let firstToInsert = blocks[0];
1269     if (isMergeable(firstToInsert)) {
1270       invariant(
1271         $isElementNode(firstBlock),
1272         "Expected 'firstBlock' to be an ElementNode",
1273       );
1274       firstBlock.append(...firstToInsert.getChildren());
1275       firstToInsert = blocks[1];
1276     }
1277     if (firstToInsert) {
1278       insertRangeAfter(firstBlock, firstToInsert);
1279     }
1280     const lastInsertedBlock = $getAncestor(nodeToSelect, INTERNAL_$isBlock)!;
1281
1282     if (
1283       insertedParagraph &&
1284       $isElementNode(lastInsertedBlock) &&
1285       (insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert))
1286     ) {
1287       lastInsertedBlock.append(...insertedParagraph.getChildren());
1288       insertedParagraph.remove();
1289     }
1290     if ($isElementNode(firstBlock) && firstBlock.isEmpty()) {
1291       firstBlock.remove();
1292     }
1293
1294     nodeToSelect.selectEnd();
1295
1296     // To understand this take a look at the test "can wrap post-linebreak nodes into new element"
1297     const lastChild = $isElementNode(firstBlock)
1298       ? firstBlock.getLastChild()
1299       : null;
1300     if ($isLineBreakNode(lastChild) && lastInsertedBlock !== firstBlock) {
1301       lastChild.remove();
1302     }
1303   }
1304
1305   /**
1306    * Inserts a new ParagraphNode into the EditorState at the current Selection
1307    *
1308    * @returns the newly inserted node.
1309    */
1310   insertParagraph(): ElementNode | null {
1311     if (this.anchor.key === 'root') {
1312       const paragraph = $createParagraphNode();
1313       $getRoot().splice(this.anchor.offset, 0, [paragraph]);
1314       paragraph.select();
1315       return paragraph;
1316     }
1317     const index = $removeTextAndSplitBlock(this);
1318     const block = $getAncestor(this.anchor.getNode(), INTERNAL_$isBlock)!;
1319     invariant($isElementNode(block), 'Expected ancestor to be an ElementNode');
1320     const firstToAppend = block.getChildAtIndex(index);
1321     const nodesToInsert = firstToAppend
1322       ? [firstToAppend, ...firstToAppend.getNextSiblings()]
1323       : [];
1324     const newBlock = block.insertNewAfter(this, false) as ElementNode | null;
1325     if (newBlock) {
1326       newBlock.append(...nodesToInsert);
1327       newBlock.selectStart();
1328       return newBlock;
1329     }
1330     // if newBlock is null, it means that block is of type CodeNode.
1331     return null;
1332   }
1333
1334   /**
1335    * Inserts a logical linebreak, which may be a new LineBreakNode or a new ParagraphNode, into the EditorState at the
1336    * current Selection.
1337    */
1338   insertLineBreak(selectStart?: boolean): void {
1339     const lineBreak = $createLineBreakNode();
1340     this.insertNodes([lineBreak]);
1341     // this is used in MacOS with the command 'ctrl-O' (openLineBreak)
1342     if (selectStart) {
1343       const parent = lineBreak.getParentOrThrow();
1344       const index = lineBreak.getIndexWithinParent();
1345       parent.select(index, index);
1346     }
1347   }
1348
1349   /**
1350    * Extracts the nodes in the Selection, splitting nodes where necessary
1351    * to get offset-level precision.
1352    *
1353    * @returns The nodes in the Selection
1354    */
1355   extract(): Array<LexicalNode> {
1356     const selectedNodes = this.getNodes();
1357     const selectedNodesLength = selectedNodes.length;
1358     const lastIndex = selectedNodesLength - 1;
1359     const anchor = this.anchor;
1360     const focus = this.focus;
1361     let firstNode = selectedNodes[0];
1362     let lastNode = selectedNodes[lastIndex];
1363     const [anchorOffset, focusOffset] = $getCharacterOffsets(this);
1364
1365     if (selectedNodesLength === 0) {
1366       return [];
1367     } else if (selectedNodesLength === 1) {
1368       if ($isTextNode(firstNode) && !this.isCollapsed()) {
1369         const startOffset =
1370           anchorOffset > focusOffset ? focusOffset : anchorOffset;
1371         const endOffset =
1372           anchorOffset > focusOffset ? anchorOffset : focusOffset;
1373         const splitNodes = firstNode.splitText(startOffset, endOffset);
1374         const node = startOffset === 0 ? splitNodes[0] : splitNodes[1];
1375         return node != null ? [node] : [];
1376       }
1377       return [firstNode];
1378     }
1379     const isBefore = anchor.isBefore(focus);
1380
1381     if ($isTextNode(firstNode)) {
1382       const startOffset = isBefore ? anchorOffset : focusOffset;
1383       if (startOffset === firstNode.getTextContentSize()) {
1384         selectedNodes.shift();
1385       } else if (startOffset !== 0) {
1386         [, firstNode] = firstNode.splitText(startOffset);
1387         selectedNodes[0] = firstNode;
1388       }
1389     }
1390     if ($isTextNode(lastNode)) {
1391       const lastNodeText = lastNode.getTextContent();
1392       const lastNodeTextLength = lastNodeText.length;
1393       const endOffset = isBefore ? focusOffset : anchorOffset;
1394       if (endOffset === 0) {
1395         selectedNodes.pop();
1396       } else if (endOffset !== lastNodeTextLength) {
1397         [lastNode] = lastNode.splitText(endOffset);
1398         selectedNodes[lastIndex] = lastNode;
1399       }
1400     }
1401     return selectedNodes;
1402   }
1403
1404   /**
1405    * Modifies the Selection according to the parameters and a set of heuristics that account for
1406    * various node types. Can be used to safely move or extend selection by one logical "unit" without
1407    * dealing explicitly with all the possible node types.
1408    *
1409    * @param alter the type of modification to perform
1410    * @param isBackward whether or not selection is backwards
1411    * @param granularity the granularity at which to apply the modification
1412    */
1413   modify(
1414     alter: 'move' | 'extend',
1415     isBackward: boolean,
1416     granularity: 'character' | 'word' | 'lineboundary',
1417   ): void {
1418     const focus = this.focus;
1419     const anchor = this.anchor;
1420     const collapse = alter === 'move';
1421
1422     // Handle the selection movement around decorators.
1423     const possibleNode = $getAdjacentNode(focus, isBackward);
1424     if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {
1425       // Make it possible to move selection from range selection to
1426       // node selection on the node.
1427       if (collapse && possibleNode.isKeyboardSelectable()) {
1428         const nodeSelection = $createNodeSelection();
1429         nodeSelection.add(possibleNode.__key);
1430         $setSelection(nodeSelection);
1431         return;
1432       }
1433       const sibling = isBackward
1434         ? possibleNode.getPreviousSibling()
1435         : possibleNode.getNextSibling();
1436
1437       if (!$isTextNode(sibling)) {
1438         const parent = possibleNode.getParentOrThrow();
1439         let offset;
1440         let elementKey;
1441
1442         if ($isElementNode(sibling)) {
1443           elementKey = sibling.__key;
1444           offset = isBackward ? sibling.getChildrenSize() : 0;
1445         } else {
1446           offset = possibleNode.getIndexWithinParent();
1447           elementKey = parent.__key;
1448           if (!isBackward) {
1449             offset++;
1450           }
1451         }
1452         focus.set(elementKey, offset, 'element');
1453         if (collapse) {
1454           anchor.set(elementKey, offset, 'element');
1455         }
1456         return;
1457       } else {
1458         const siblingKey = sibling.__key;
1459         const offset = isBackward ? sibling.getTextContent().length : 0;
1460         focus.set(siblingKey, offset, 'text');
1461         if (collapse) {
1462           anchor.set(siblingKey, offset, 'text');
1463         }
1464         return;
1465       }
1466     }
1467     const editor = getActiveEditor();
1468     const domSelection = getDOMSelection(editor._window);
1469
1470     if (!domSelection) {
1471       return;
1472     }
1473     const blockCursorElement = editor._blockCursorElement;
1474     const rootElement = editor._rootElement;
1475     // Remove the block cursor element if it exists. This will ensure selection
1476     // works as intended. If we leave it in the DOM all sorts of strange bugs
1477     // occur. :/
1478     if (
1479       rootElement !== null &&
1480       blockCursorElement !== null &&
1481       $isElementNode(possibleNode) &&
1482       !possibleNode.isInline() &&
1483       !possibleNode.canBeEmpty()
1484     ) {
1485       removeDOMBlockCursorElement(blockCursorElement, editor, rootElement);
1486     }
1487     // We use the DOM selection.modify API here to "tell" us what the selection
1488     // will be. We then use it to update the Lexical selection accordingly. This
1489     // is much more reliable than waiting for a beforeinput and using the ranges
1490     // from getTargetRanges(), and is also better than trying to do it ourselves
1491     // using Intl.Segmenter or other workarounds that struggle with word segments
1492     // and line segments (especially with word wrapping and non-Roman languages).
1493     moveNativeSelection(
1494       domSelection,
1495       alter,
1496       isBackward ? 'backward' : 'forward',
1497       granularity,
1498     );
1499     // Guard against no ranges
1500     if (domSelection.rangeCount > 0) {
1501       const range = domSelection.getRangeAt(0);
1502       // Apply the DOM selection to our Lexical selection.
1503       const anchorNode = this.anchor.getNode();
1504       const root = $isRootNode(anchorNode)
1505         ? anchorNode
1506         : $getNearestRootOrShadowRoot(anchorNode);
1507       this.applyDOMRange(range);
1508       this.dirty = true;
1509       if (!collapse) {
1510         // Validate selection; make sure that the new extended selection respects shadow roots
1511         const nodes = this.getNodes();
1512         const validNodes = [];
1513         let shrinkSelection = false;
1514         for (let i = 0; i < nodes.length; i++) {
1515           const nextNode = nodes[i];
1516           if ($hasAncestor(nextNode, root)) {
1517             validNodes.push(nextNode);
1518           } else {
1519             shrinkSelection = true;
1520           }
1521         }
1522         if (shrinkSelection && validNodes.length > 0) {
1523           // validNodes length check is a safeguard against an invalid selection; as getNodes()
1524           // will return an empty array in this case
1525           if (isBackward) {
1526             const firstValidNode = validNodes[0];
1527             if ($isElementNode(firstValidNode)) {
1528               firstValidNode.selectStart();
1529             } else {
1530               firstValidNode.getParentOrThrow().selectStart();
1531             }
1532           } else {
1533             const lastValidNode = validNodes[validNodes.length - 1];
1534             if ($isElementNode(lastValidNode)) {
1535               lastValidNode.selectEnd();
1536             } else {
1537               lastValidNode.getParentOrThrow().selectEnd();
1538             }
1539           }
1540         }
1541
1542         // Because a range works on start and end, we might need to flip
1543         // the anchor and focus points to match what the DOM has, not what
1544         // the range has specifically.
1545         if (
1546           domSelection.anchorNode !== range.startContainer ||
1547           domSelection.anchorOffset !== range.startOffset
1548         ) {
1549           $swapPoints(this);
1550         }
1551       }
1552     }
1553   }
1554   /**
1555    * Helper for handling forward character and word deletion that prevents element nodes
1556    * like a table, columns layout being destroyed
1557    *
1558    * @param anchor the anchor
1559    * @param anchorNode the anchor node in the selection
1560    * @param isBackward whether or not selection is backwards
1561    */
1562   forwardDeletion(
1563     anchor: PointType,
1564     anchorNode: TextNode | ElementNode,
1565     isBackward: boolean,
1566   ): boolean {
1567     if (
1568       !isBackward &&
1569       // Delete forward handle case
1570       ((anchor.type === 'element' &&
1571         $isElementNode(anchorNode) &&
1572         anchor.offset === anchorNode.getChildrenSize()) ||
1573         (anchor.type === 'text' &&
1574           anchor.offset === anchorNode.getTextContentSize()))
1575     ) {
1576       const parent = anchorNode.getParent();
1577       const nextSibling =
1578         anchorNode.getNextSibling() ||
1579         (parent === null ? null : parent.getNextSibling());
1580
1581       if ($isElementNode(nextSibling) && nextSibling.isShadowRoot()) {
1582         return true;
1583       }
1584     }
1585     return false;
1586   }
1587
1588   /**
1589    * Performs one logical character deletion operation on the EditorState based on the current Selection.
1590    * Handles different node types.
1591    *
1592    * @param isBackward whether or not the selection is backwards.
1593    */
1594   deleteCharacter(isBackward: boolean): void {
1595     const wasCollapsed = this.isCollapsed();
1596     if (this.isCollapsed()) {
1597       const anchor = this.anchor;
1598       let anchorNode: TextNode | ElementNode | null = anchor.getNode();
1599       if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
1600         return;
1601       }
1602
1603       // Handle the deletion around decorators.
1604       const focus = this.focus;
1605       const possibleNode = $getAdjacentNode(focus, isBackward);
1606       if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {
1607         // Make it possible to move selection from range selection to
1608         // node selection on the node.
1609         if (
1610           possibleNode.isKeyboardSelectable() &&
1611           $isElementNode(anchorNode) &&
1612           anchorNode.getChildrenSize() === 0
1613         ) {
1614           anchorNode.remove();
1615           const nodeSelection = $createNodeSelection();
1616           nodeSelection.add(possibleNode.__key);
1617           $setSelection(nodeSelection);
1618         } else {
1619           possibleNode.remove();
1620           const editor = getActiveEditor();
1621           editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1622         }
1623         return;
1624       } else if (
1625         !isBackward &&
1626         $isElementNode(possibleNode) &&
1627         $isElementNode(anchorNode) &&
1628         anchorNode.isEmpty()
1629       ) {
1630         anchorNode.remove();
1631         possibleNode.selectStart();
1632         return;
1633       }
1634       this.modify('extend', isBackward, 'character');
1635
1636       if (!this.isCollapsed()) {
1637         const focusNode = focus.type === 'text' ? focus.getNode() : null;
1638         anchorNode = anchor.type === 'text' ? anchor.getNode() : null;
1639
1640         if (focusNode !== null && focusNode.isSegmented()) {
1641           const offset = focus.offset;
1642           const textContentSize = focusNode.getTextContentSize();
1643           if (
1644             focusNode.is(anchorNode) ||
1645             (isBackward && offset !== textContentSize) ||
1646             (!isBackward && offset !== 0)
1647           ) {
1648             $removeSegment(focusNode, isBackward, offset);
1649             return;
1650           }
1651         } else if (anchorNode !== null && anchorNode.isSegmented()) {
1652           const offset = anchor.offset;
1653           const textContentSize = anchorNode.getTextContentSize();
1654           if (
1655             anchorNode.is(focusNode) ||
1656             (isBackward && offset !== 0) ||
1657             (!isBackward && offset !== textContentSize)
1658           ) {
1659             $removeSegment(anchorNode, isBackward, offset);
1660             return;
1661           }
1662         }
1663         $updateCaretSelectionForUnicodeCharacter(this, isBackward);
1664       } else if (isBackward && anchor.offset === 0) {
1665         // Special handling around rich text nodes
1666         const element =
1667           anchor.type === 'element'
1668             ? anchor.getNode()
1669             : anchor.getNode().getParentOrThrow();
1670         if (element.collapseAtStart(this)) {
1671           return;
1672         }
1673       }
1674     }
1675     this.removeText();
1676     if (
1677       isBackward &&
1678       !wasCollapsed &&
1679       this.isCollapsed() &&
1680       this.anchor.type === 'element' &&
1681       this.anchor.offset === 0
1682     ) {
1683       const anchorNode = this.anchor.getNode();
1684       if (
1685         anchorNode.isEmpty() &&
1686         $isRootNode(anchorNode.getParent()) &&
1687         anchorNode.getIndexWithinParent() === 0
1688       ) {
1689         anchorNode.collapseAtStart(this);
1690       }
1691     }
1692   }
1693
1694   /**
1695    * Performs one logical line deletion operation on the EditorState based on the current Selection.
1696    * Handles different node types.
1697    *
1698    * @param isBackward whether or not the selection is backwards.
1699    */
1700   deleteLine(isBackward: boolean): void {
1701     if (this.isCollapsed()) {
1702       // Since `domSelection.modify('extend', ..., 'lineboundary')` works well for text selections
1703       // but doesn't properly handle selections which end on elements, a space character is added
1704       // for such selections transforming their anchor's type to 'text'
1705       const anchorIsElement = this.anchor.type === 'element';
1706       if (anchorIsElement) {
1707         this.insertText(' ');
1708       }
1709
1710       this.modify('extend', isBackward, 'lineboundary');
1711
1712       // If selection is extended to cover text edge then extend it one character more
1713       // to delete its parent element. Otherwise text content will be deleted but empty
1714       // parent node will remain
1715       const endPoint = isBackward ? this.focus : this.anchor;
1716       if (endPoint.offset === 0) {
1717         this.modify('extend', isBackward, 'character');
1718       }
1719
1720       // Adjusts selection to include an extra character added for element anchors to remove it
1721       if (anchorIsElement) {
1722         const startPoint = isBackward ? this.anchor : this.focus;
1723         startPoint.set(startPoint.key, startPoint.offset + 1, startPoint.type);
1724       }
1725     }
1726     this.removeText();
1727   }
1728
1729   /**
1730    * Performs one logical word deletion operation on the EditorState based on the current Selection.
1731    * Handles different node types.
1732    *
1733    * @param isBackward whether or not the selection is backwards.
1734    */
1735   deleteWord(isBackward: boolean): void {
1736     if (this.isCollapsed()) {
1737       const anchor = this.anchor;
1738       const anchorNode: TextNode | ElementNode | null = anchor.getNode();
1739       if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
1740         return;
1741       }
1742       this.modify('extend', isBackward, 'word');
1743     }
1744     this.removeText();
1745   }
1746
1747   /**
1748    * Returns whether the Selection is "backwards", meaning the focus
1749    * logically precedes the anchor in the EditorState.
1750    * @returns true if the Selection is backwards, false otherwise.
1751    */
1752   isBackward(): boolean {
1753     return this.focus.isBefore(this.anchor);
1754   }
1755
1756   getStartEndPoints(): null | [PointType, PointType] {
1757     return [this.anchor, this.focus];
1758   }
1759 }
1760
1761 export function $isNodeSelection(x: unknown): x is NodeSelection {
1762   return x instanceof NodeSelection;
1763 }
1764
1765 function getCharacterOffset(point: PointType): number {
1766   const offset = point.offset;
1767   if (point.type === 'text') {
1768     return offset;
1769   }
1770
1771   const parent = point.getNode();
1772   return offset === parent.getChildrenSize()
1773     ? parent.getTextContent().length
1774     : 0;
1775 }
1776
1777 export function $getCharacterOffsets(
1778   selection: BaseSelection,
1779 ): [number, number] {
1780   const anchorAndFocus = selection.getStartEndPoints();
1781   if (anchorAndFocus === null) {
1782     return [0, 0];
1783   }
1784   const [anchor, focus] = anchorAndFocus;
1785   if (
1786     anchor.type === 'element' &&
1787     focus.type === 'element' &&
1788     anchor.key === focus.key &&
1789     anchor.offset === focus.offset
1790   ) {
1791     return [0, 0];
1792   }
1793   return [getCharacterOffset(anchor), getCharacterOffset(focus)];
1794 }
1795
1796 function $swapPoints(selection: RangeSelection): void {
1797   const focus = selection.focus;
1798   const anchor = selection.anchor;
1799   const anchorKey = anchor.key;
1800   const anchorOffset = anchor.offset;
1801   const anchorType = anchor.type;
1802
1803   $setPointValues(anchor, focus.key, focus.offset, focus.type);
1804   $setPointValues(focus, anchorKey, anchorOffset, anchorType);
1805   selection._cachedNodes = null;
1806 }
1807
1808 function moveNativeSelection(
1809   domSelection: Selection,
1810   alter: 'move' | 'extend',
1811   direction: 'backward' | 'forward' | 'left' | 'right',
1812   granularity: 'character' | 'word' | 'lineboundary',
1813 ): void {
1814   // Selection.modify() method applies a change to the current selection or cursor position,
1815   // but is still non-standard in some browsers.
1816   domSelection.modify(alter, direction, granularity);
1817 }
1818
1819 function $updateCaretSelectionForUnicodeCharacter(
1820   selection: RangeSelection,
1821   isBackward: boolean,
1822 ): void {
1823   const anchor = selection.anchor;
1824   const focus = selection.focus;
1825   const anchorNode = anchor.getNode();
1826   const focusNode = focus.getNode();
1827
1828   if (
1829     anchorNode === focusNode &&
1830     anchor.type === 'text' &&
1831     focus.type === 'text'
1832   ) {
1833     // Handling of multibyte characters
1834     const anchorOffset = anchor.offset;
1835     const focusOffset = focus.offset;
1836     const isBefore = anchorOffset < focusOffset;
1837     const startOffset = isBefore ? anchorOffset : focusOffset;
1838     const endOffset = isBefore ? focusOffset : anchorOffset;
1839     const characterOffset = endOffset - 1;
1840
1841     if (startOffset !== characterOffset) {
1842       const text = anchorNode.getTextContent().slice(startOffset, endOffset);
1843       if (!doesContainGrapheme(text)) {
1844         if (isBackward) {
1845           focus.offset = characterOffset;
1846         } else {
1847           anchor.offset = characterOffset;
1848         }
1849       }
1850     }
1851   } else {
1852     // TODO Handling of multibyte characters
1853   }
1854 }
1855
1856 function $removeSegment(
1857   node: TextNode,
1858   isBackward: boolean,
1859   offset: number,
1860 ): void {
1861   const textNode = node;
1862   const textContent = textNode.getTextContent();
1863   const split = textContent.split(/(?=\s)/g);
1864   const splitLength = split.length;
1865   let segmentOffset = 0;
1866   let restoreOffset: number | undefined = 0;
1867
1868   for (let i = 0; i < splitLength; i++) {
1869     const text = split[i];
1870     const isLast = i === splitLength - 1;
1871     restoreOffset = segmentOffset;
1872     segmentOffset += text.length;
1873
1874     if (
1875       (isBackward && segmentOffset === offset) ||
1876       segmentOffset > offset ||
1877       isLast
1878     ) {
1879       split.splice(i, 1);
1880       if (isLast) {
1881         restoreOffset = undefined;
1882       }
1883       break;
1884     }
1885   }
1886   const nextTextContent = split.join('').trim();
1887
1888   if (nextTextContent === '') {
1889     textNode.remove();
1890   } else {
1891     textNode.setTextContent(nextTextContent);
1892     textNode.select(restoreOffset, restoreOffset);
1893   }
1894 }
1895
1896 function shouldResolveAncestor(
1897   resolvedElement: ElementNode,
1898   resolvedOffset: number,
1899   lastPoint: null | PointType,
1900 ): boolean {
1901   const parent = resolvedElement.getParent();
1902   return (
1903     lastPoint === null ||
1904     parent === null ||
1905     !parent.canBeEmpty() ||
1906     parent !== lastPoint.getNode()
1907   );
1908 }
1909
1910 function $internalResolveSelectionPoint(
1911   dom: Node,
1912   offset: number,
1913   lastPoint: null | PointType,
1914   editor: LexicalEditor,
1915 ): null | PointType {
1916   let resolvedOffset = offset;
1917   let resolvedNode: TextNode | LexicalNode | null;
1918   // If we have selection on an element, we will
1919   // need to figure out (using the offset) what text
1920   // node should be selected.
1921
1922   if (dom.nodeType === DOM_ELEMENT_TYPE) {
1923     // Resolve element to a ElementNode, or TextNode, or null
1924     let moveSelectionToEnd = false;
1925     // Given we're moving selection to another node, selection is
1926     // definitely dirty.
1927     // We use the anchor to find which child node to select
1928     const childNodes = dom.childNodes;
1929     const childNodesLength = childNodes.length;
1930     const blockCursorElement = editor._blockCursorElement;
1931     // If the anchor is the same as length, then this means we
1932     // need to select the very last text node.
1933     if (resolvedOffset === childNodesLength) {
1934       moveSelectionToEnd = true;
1935       resolvedOffset = childNodesLength - 1;
1936     }
1937     let childDOM = childNodes[resolvedOffset];
1938     let hasBlockCursor = false;
1939     if (childDOM === blockCursorElement) {
1940       childDOM = childNodes[resolvedOffset + 1];
1941       hasBlockCursor = true;
1942     } else if (blockCursorElement !== null) {
1943       const blockCursorElementParent = blockCursorElement.parentNode;
1944       if (dom === blockCursorElementParent) {
1945         const blockCursorOffset = Array.prototype.indexOf.call(
1946           blockCursorElementParent.children,
1947           blockCursorElement,
1948         );
1949         if (offset > blockCursorOffset) {
1950           resolvedOffset--;
1951         }
1952       }
1953     }
1954     resolvedNode = $getNodeFromDOM(childDOM);
1955
1956     if ($isTextNode(resolvedNode)) {
1957       resolvedOffset = getTextNodeOffset(resolvedNode, moveSelectionToEnd);
1958     } else {
1959       let resolvedElement = $getNodeFromDOM(dom);
1960       // Ensure resolvedElement is actually a element.
1961       if (resolvedElement === null) {
1962         return null;
1963       }
1964       if ($isElementNode(resolvedElement)) {
1965         resolvedOffset = Math.min(
1966           resolvedElement.getChildrenSize(),
1967           resolvedOffset,
1968         );
1969         let child = resolvedElement.getChildAtIndex(resolvedOffset);
1970         if (
1971           $isElementNode(child) &&
1972           shouldResolveAncestor(child, resolvedOffset, lastPoint)
1973         ) {
1974           const descendant = moveSelectionToEnd
1975             ? child.getLastDescendant()
1976             : child.getFirstDescendant();
1977           if (descendant === null) {
1978             resolvedElement = child;
1979           } else {
1980             child = descendant;
1981             resolvedElement = $isElementNode(child)
1982               ? child
1983               : child.getParentOrThrow();
1984           }
1985           resolvedOffset = 0;
1986         }
1987         if ($isTextNode(child)) {
1988           resolvedNode = child;
1989           resolvedElement = null;
1990           resolvedOffset = getTextNodeOffset(child, moveSelectionToEnd);
1991         } else if (
1992           child !== resolvedElement &&
1993           moveSelectionToEnd &&
1994           !hasBlockCursor
1995         ) {
1996           resolvedOffset++;
1997         }
1998       } else {
1999         const index = resolvedElement.getIndexWithinParent();
2000         // When selecting decorators, there can be some selection issues when using resolvedOffset,
2001         // and instead we should be checking if we're using the offset
2002         if (
2003           offset === 0 &&
2004           $isDecoratorNode(resolvedElement) &&
2005           $getNodeFromDOM(dom) === resolvedElement
2006         ) {
2007           resolvedOffset = index;
2008         } else {
2009           resolvedOffset = index + 1;
2010         }
2011         resolvedElement = resolvedElement.getParentOrThrow();
2012       }
2013       if ($isElementNode(resolvedElement)) {
2014         return $createPoint(resolvedElement.__key, resolvedOffset, 'element');
2015       }
2016     }
2017   } else {
2018     // TextNode or null
2019     resolvedNode = $getNodeFromDOM(dom);
2020   }
2021   if (!$isTextNode(resolvedNode)) {
2022     return null;
2023   }
2024   return $createPoint(resolvedNode.__key, resolvedOffset, 'text');
2025 }
2026
2027 function resolveSelectionPointOnBoundary(
2028   point: TextPointType,
2029   isBackward: boolean,
2030   isCollapsed: boolean,
2031 ): void {
2032   const offset = point.offset;
2033   const node = point.getNode();
2034
2035   if (offset === 0) {
2036     const prevSibling = node.getPreviousSibling();
2037     const parent = node.getParent();
2038
2039     if (!isBackward) {
2040       if (
2041         $isElementNode(prevSibling) &&
2042         !isCollapsed &&
2043         prevSibling.isInline()
2044       ) {
2045         point.key = prevSibling.__key;
2046         point.offset = prevSibling.getChildrenSize();
2047         // @ts-expect-error: intentional
2048         point.type = 'element';
2049       } else if ($isTextNode(prevSibling)) {
2050         point.key = prevSibling.__key;
2051         point.offset = prevSibling.getTextContent().length;
2052       }
2053     } else if (
2054       (isCollapsed || !isBackward) &&
2055       prevSibling === null &&
2056       $isElementNode(parent) &&
2057       parent.isInline()
2058     ) {
2059       const parentSibling = parent.getPreviousSibling();
2060       if ($isTextNode(parentSibling)) {
2061         point.key = parentSibling.__key;
2062         point.offset = parentSibling.getTextContent().length;
2063       }
2064     }
2065   } else if (offset === node.getTextContent().length) {
2066     const nextSibling = node.getNextSibling();
2067     const parent = node.getParent();
2068
2069     if (isBackward && $isElementNode(nextSibling) && nextSibling.isInline()) {
2070       point.key = nextSibling.__key;
2071       point.offset = 0;
2072       // @ts-expect-error: intentional
2073       point.type = 'element';
2074     } else if (
2075       (isCollapsed || isBackward) &&
2076       nextSibling === null &&
2077       $isElementNode(parent) &&
2078       parent.isInline() &&
2079       !parent.canInsertTextAfter()
2080     ) {
2081       const parentSibling = parent.getNextSibling();
2082       if ($isTextNode(parentSibling)) {
2083         point.key = parentSibling.__key;
2084         point.offset = 0;
2085       }
2086     }
2087   }
2088 }
2089
2090 function $normalizeSelectionPointsForBoundaries(
2091   anchor: PointType,
2092   focus: PointType,
2093   lastSelection: null | BaseSelection,
2094 ): void {
2095   if (anchor.type === 'text' && focus.type === 'text') {
2096     const isBackward = anchor.isBefore(focus);
2097     const isCollapsed = anchor.is(focus);
2098
2099     // Attempt to normalize the offset to the previous sibling if we're at the
2100     // start of a text node and the sibling is a text node or inline element.
2101     resolveSelectionPointOnBoundary(anchor, isBackward, isCollapsed);
2102     resolveSelectionPointOnBoundary(focus, !isBackward, isCollapsed);
2103
2104     if (isCollapsed) {
2105       focus.key = anchor.key;
2106       focus.offset = anchor.offset;
2107       focus.type = anchor.type;
2108     }
2109     const editor = getActiveEditor();
2110
2111     if (
2112       editor.isComposing() &&
2113       editor._compositionKey !== anchor.key &&
2114       $isRangeSelection(lastSelection)
2115     ) {
2116       const lastAnchor = lastSelection.anchor;
2117       const lastFocus = lastSelection.focus;
2118       $setPointValues(
2119         anchor,
2120         lastAnchor.key,
2121         lastAnchor.offset,
2122         lastAnchor.type,
2123       );
2124       $setPointValues(focus, lastFocus.key, lastFocus.offset, lastFocus.type);
2125     }
2126   }
2127 }
2128
2129 function $internalResolveSelectionPoints(
2130   anchorDOM: null | Node,
2131   anchorOffset: number,
2132   focusDOM: null | Node,
2133   focusOffset: number,
2134   editor: LexicalEditor,
2135   lastSelection: null | BaseSelection,
2136 ): null | [PointType, PointType] {
2137   if (
2138     anchorDOM === null ||
2139     focusDOM === null ||
2140     !isSelectionWithinEditor(editor, anchorDOM, focusDOM)
2141   ) {
2142     return null;
2143   }
2144   const resolvedAnchorPoint = $internalResolveSelectionPoint(
2145     anchorDOM,
2146     anchorOffset,
2147     $isRangeSelection(lastSelection) ? lastSelection.anchor : null,
2148     editor,
2149   );
2150   if (resolvedAnchorPoint === null) {
2151     return null;
2152   }
2153   const resolvedFocusPoint = $internalResolveSelectionPoint(
2154     focusDOM,
2155     focusOffset,
2156     $isRangeSelection(lastSelection) ? lastSelection.focus : null,
2157     editor,
2158   );
2159   if (resolvedFocusPoint === null) {
2160     return null;
2161   }
2162   if (
2163     resolvedAnchorPoint.type === 'element' &&
2164     resolvedFocusPoint.type === 'element'
2165   ) {
2166     const anchorNode = $getNodeFromDOM(anchorDOM);
2167     const focusNode = $getNodeFromDOM(focusDOM);
2168     // Ensure if we're selecting the content of a decorator that we
2169     // return null for this point, as it's not in the controlled scope
2170     // of Lexical.
2171     if ($isDecoratorNode(anchorNode) && $isDecoratorNode(focusNode)) {
2172       return null;
2173     }
2174   }
2175
2176   // Handle normalization of selection when it is at the boundaries.
2177   $normalizeSelectionPointsForBoundaries(
2178     resolvedAnchorPoint,
2179     resolvedFocusPoint,
2180     lastSelection,
2181   );
2182
2183   return [resolvedAnchorPoint, resolvedFocusPoint];
2184 }
2185
2186 export function $isBlockElementNode(
2187   node: LexicalNode | null | undefined,
2188 ): node is ElementNode {
2189   return $isElementNode(node) && !node.isInline();
2190 }
2191
2192 // This is used to make a selection when the existing
2193 // selection is null, i.e. forcing selection on the editor
2194 // when it current exists outside the editor.
2195
2196 export function $internalMakeRangeSelection(
2197   anchorKey: NodeKey,
2198   anchorOffset: number,
2199   focusKey: NodeKey,
2200   focusOffset: number,
2201   anchorType: 'text' | 'element',
2202   focusType: 'text' | 'element',
2203 ): RangeSelection {
2204   const editorState = getActiveEditorState();
2205   const selection = new RangeSelection(
2206     $createPoint(anchorKey, anchorOffset, anchorType),
2207     $createPoint(focusKey, focusOffset, focusType),
2208     0,
2209     '',
2210   );
2211   selection.dirty = true;
2212   editorState._selection = selection;
2213   return selection;
2214 }
2215
2216 export function $createRangeSelection(): RangeSelection {
2217   const anchor = $createPoint('root', 0, 'element');
2218   const focus = $createPoint('root', 0, 'element');
2219   return new RangeSelection(anchor, focus, 0, '');
2220 }
2221
2222 export function $createNodeSelection(): NodeSelection {
2223   return new NodeSelection(new Set());
2224 }
2225
2226 export function $internalCreateSelection(
2227   editor: LexicalEditor,
2228 ): null | BaseSelection {
2229   const currentEditorState = editor.getEditorState();
2230   const lastSelection = currentEditorState._selection;
2231   const domSelection = getDOMSelection(editor._window);
2232
2233   if ($isRangeSelection(lastSelection) || lastSelection == null) {
2234     return $internalCreateRangeSelection(
2235       lastSelection,
2236       domSelection,
2237       editor,
2238       null,
2239     );
2240   }
2241   return lastSelection.clone();
2242 }
2243
2244 export function $createRangeSelectionFromDom(
2245   domSelection: Selection | null,
2246   editor: LexicalEditor,
2247 ): null | RangeSelection {
2248   return $internalCreateRangeSelection(null, domSelection, editor, null);
2249 }
2250
2251 export function $internalCreateRangeSelection(
2252   lastSelection: null | BaseSelection,
2253   domSelection: Selection | null,
2254   editor: LexicalEditor,
2255   event: UIEvent | Event | null,
2256 ): null | RangeSelection {
2257   const windowObj = editor._window;
2258   if (windowObj === null) {
2259     return null;
2260   }
2261   // When we create a selection, we try to use the previous
2262   // selection where possible, unless an actual user selection
2263   // change has occurred. When we do need to create a new selection
2264   // we validate we can have text nodes for both anchor and focus
2265   // nodes. If that holds true, we then return that selection
2266   // as a mutable object that we use for the editor state for this
2267   // update cycle. If a selection gets changed, and requires a
2268   // update to native DOM selection, it gets marked as "dirty".
2269   // If the selection changes, but matches with the existing
2270   // DOM selection, then we only need to sync it. Otherwise,
2271   // we generally bail out of doing an update to selection during
2272   // reconciliation unless there are dirty nodes that need
2273   // reconciling.
2274
2275   const windowEvent = event || windowObj.event;
2276   const eventType = windowEvent ? windowEvent.type : undefined;
2277   const isSelectionChange = eventType === 'selectionchange';
2278   const useDOMSelection =
2279     !getIsProcessingMutations() &&
2280     (isSelectionChange ||
2281       eventType === 'beforeinput' ||
2282       eventType === 'compositionstart' ||
2283       eventType === 'compositionend' ||
2284       (eventType === 'click' &&
2285         windowEvent &&
2286         (windowEvent as InputEvent).detail === 3) ||
2287       eventType === 'drop' ||
2288       eventType === undefined);
2289   let anchorDOM, focusDOM, anchorOffset, focusOffset;
2290
2291   if (!$isRangeSelection(lastSelection) || useDOMSelection) {
2292     if (domSelection === null) {
2293       return null;
2294     }
2295     anchorDOM = domSelection.anchorNode;
2296     focusDOM = domSelection.focusNode;
2297     anchorOffset = domSelection.anchorOffset;
2298     focusOffset = domSelection.focusOffset;
2299     if (
2300       isSelectionChange &&
2301       $isRangeSelection(lastSelection) &&
2302       !isSelectionWithinEditor(editor, anchorDOM, focusDOM)
2303     ) {
2304       return lastSelection.clone();
2305     }
2306   } else {
2307     return lastSelection.clone();
2308   }
2309   // Let's resolve the text nodes from the offsets and DOM nodes we have from
2310   // native selection.
2311   const resolvedSelectionPoints = $internalResolveSelectionPoints(
2312     anchorDOM,
2313     anchorOffset,
2314     focusDOM,
2315     focusOffset,
2316     editor,
2317     lastSelection,
2318   );
2319   if (resolvedSelectionPoints === null) {
2320     return null;
2321   }
2322   const [resolvedAnchorPoint, resolvedFocusPoint] = resolvedSelectionPoints;
2323   return new RangeSelection(
2324     resolvedAnchorPoint,
2325     resolvedFocusPoint,
2326     !$isRangeSelection(lastSelection) ? 0 : lastSelection.format,
2327     !$isRangeSelection(lastSelection) ? '' : lastSelection.style,
2328   );
2329 }
2330
2331 export function $getSelection(): null | BaseSelection {
2332   const editorState = getActiveEditorState();
2333   return editorState._selection;
2334 }
2335
2336 export function $getPreviousSelection(): null | BaseSelection {
2337   const editor = getActiveEditor();
2338   return editor._editorState._selection;
2339 }
2340
2341 export function $updateElementSelectionOnCreateDeleteNode(
2342   selection: RangeSelection,
2343   parentNode: LexicalNode,
2344   nodeOffset: number,
2345   times = 1,
2346 ): void {
2347   const anchor = selection.anchor;
2348   const focus = selection.focus;
2349   const anchorNode = anchor.getNode();
2350   const focusNode = focus.getNode();
2351   if (!parentNode.is(anchorNode) && !parentNode.is(focusNode)) {
2352     return;
2353   }
2354   const parentKey = parentNode.__key;
2355   // Single node. We shift selection but never redimension it
2356   if (selection.isCollapsed()) {
2357     const selectionOffset = anchor.offset;
2358     if (
2359       (nodeOffset <= selectionOffset && times > 0) ||
2360       (nodeOffset < selectionOffset && times < 0)
2361     ) {
2362       const newSelectionOffset = Math.max(0, selectionOffset + times);
2363       anchor.set(parentKey, newSelectionOffset, 'element');
2364       focus.set(parentKey, newSelectionOffset, 'element');
2365       // The new selection might point to text nodes, try to resolve them
2366       $updateSelectionResolveTextNodes(selection);
2367     }
2368   } else {
2369     // Multiple nodes selected. We shift or redimension selection
2370     const isBackward = selection.isBackward();
2371     const firstPoint = isBackward ? focus : anchor;
2372     const firstPointNode = firstPoint.getNode();
2373     const lastPoint = isBackward ? anchor : focus;
2374     const lastPointNode = lastPoint.getNode();
2375     if (parentNode.is(firstPointNode)) {
2376       const firstPointOffset = firstPoint.offset;
2377       if (
2378         (nodeOffset <= firstPointOffset && times > 0) ||
2379         (nodeOffset < firstPointOffset && times < 0)
2380       ) {
2381         firstPoint.set(
2382           parentKey,
2383           Math.max(0, firstPointOffset + times),
2384           'element',
2385         );
2386       }
2387     }
2388     if (parentNode.is(lastPointNode)) {
2389       const lastPointOffset = lastPoint.offset;
2390       if (
2391         (nodeOffset <= lastPointOffset && times > 0) ||
2392         (nodeOffset < lastPointOffset && times < 0)
2393       ) {
2394         lastPoint.set(
2395           parentKey,
2396           Math.max(0, lastPointOffset + times),
2397           'element',
2398         );
2399       }
2400     }
2401   }
2402   // The new selection might point to text nodes, try to resolve them
2403   $updateSelectionResolveTextNodes(selection);
2404 }
2405
2406 function $updateSelectionResolveTextNodes(selection: RangeSelection): void {
2407   const anchor = selection.anchor;
2408   const anchorOffset = anchor.offset;
2409   const focus = selection.focus;
2410   const focusOffset = focus.offset;
2411   const anchorNode = anchor.getNode();
2412   const focusNode = focus.getNode();
2413   if (selection.isCollapsed()) {
2414     if (!$isElementNode(anchorNode)) {
2415       return;
2416     }
2417     const childSize = anchorNode.getChildrenSize();
2418     const anchorOffsetAtEnd = anchorOffset >= childSize;
2419     const child = anchorOffsetAtEnd
2420       ? anchorNode.getChildAtIndex(childSize - 1)
2421       : anchorNode.getChildAtIndex(anchorOffset);
2422     if ($isTextNode(child)) {
2423       let newOffset = 0;
2424       if (anchorOffsetAtEnd) {
2425         newOffset = child.getTextContentSize();
2426       }
2427       anchor.set(child.__key, newOffset, 'text');
2428       focus.set(child.__key, newOffset, 'text');
2429     }
2430     return;
2431   }
2432   if ($isElementNode(anchorNode)) {
2433     const childSize = anchorNode.getChildrenSize();
2434     const anchorOffsetAtEnd = anchorOffset >= childSize;
2435     const child = anchorOffsetAtEnd
2436       ? anchorNode.getChildAtIndex(childSize - 1)
2437       : anchorNode.getChildAtIndex(anchorOffset);
2438     if ($isTextNode(child)) {
2439       let newOffset = 0;
2440       if (anchorOffsetAtEnd) {
2441         newOffset = child.getTextContentSize();
2442       }
2443       anchor.set(child.__key, newOffset, 'text');
2444     }
2445   }
2446   if ($isElementNode(focusNode)) {
2447     const childSize = focusNode.getChildrenSize();
2448     const focusOffsetAtEnd = focusOffset >= childSize;
2449     const child = focusOffsetAtEnd
2450       ? focusNode.getChildAtIndex(childSize - 1)
2451       : focusNode.getChildAtIndex(focusOffset);
2452     if ($isTextNode(child)) {
2453       let newOffset = 0;
2454       if (focusOffsetAtEnd) {
2455         newOffset = child.getTextContentSize();
2456       }
2457       focus.set(child.__key, newOffset, 'text');
2458     }
2459   }
2460 }
2461
2462 export function applySelectionTransforms(
2463   nextEditorState: EditorState,
2464   editor: LexicalEditor,
2465 ): void {
2466   const prevEditorState = editor.getEditorState();
2467   const prevSelection = prevEditorState._selection;
2468   const nextSelection = nextEditorState._selection;
2469   if ($isRangeSelection(nextSelection)) {
2470     const anchor = nextSelection.anchor;
2471     const focus = nextSelection.focus;
2472     let anchorNode;
2473
2474     if (anchor.type === 'text') {
2475       anchorNode = anchor.getNode();
2476       anchorNode.selectionTransform(prevSelection, nextSelection);
2477     }
2478     if (focus.type === 'text') {
2479       const focusNode = focus.getNode();
2480       if (anchorNode !== focusNode) {
2481         focusNode.selectionTransform(prevSelection, nextSelection);
2482       }
2483     }
2484   }
2485 }
2486
2487 export function moveSelectionPointToSibling(
2488   point: PointType,
2489   node: LexicalNode,
2490   parent: ElementNode,
2491   prevSibling: LexicalNode | null,
2492   nextSibling: LexicalNode | null,
2493 ): void {
2494   let siblingKey = null;
2495   let offset = 0;
2496   let type: 'text' | 'element' | null = null;
2497   if (prevSibling !== null) {
2498     siblingKey = prevSibling.__key;
2499     if ($isTextNode(prevSibling)) {
2500       offset = prevSibling.getTextContentSize();
2501       type = 'text';
2502     } else if ($isElementNode(prevSibling)) {
2503       offset = prevSibling.getChildrenSize();
2504       type = 'element';
2505     }
2506   } else {
2507     if (nextSibling !== null) {
2508       siblingKey = nextSibling.__key;
2509       if ($isTextNode(nextSibling)) {
2510         type = 'text';
2511       } else if ($isElementNode(nextSibling)) {
2512         type = 'element';
2513       }
2514     }
2515   }
2516   if (siblingKey !== null && type !== null) {
2517     point.set(siblingKey, offset, type);
2518   } else {
2519     offset = node.getIndexWithinParent();
2520     if (offset === -1) {
2521       // Move selection to end of parent
2522       offset = parent.getChildrenSize();
2523     }
2524     point.set(parent.__key, offset, 'element');
2525   }
2526 }
2527
2528 export function adjustPointOffsetForMergedSibling(
2529   point: PointType,
2530   isBefore: boolean,
2531   key: NodeKey,
2532   target: TextNode,
2533   textLength: number,
2534 ): void {
2535   if (point.type === 'text') {
2536     point.key = key;
2537     if (!isBefore) {
2538       point.offset += textLength;
2539     }
2540   } else if (point.offset > target.getIndexWithinParent()) {
2541     point.offset -= 1;
2542   }
2543 }
2544
2545 export function updateDOMSelection(
2546   prevSelection: BaseSelection | null,
2547   nextSelection: BaseSelection | null,
2548   editor: LexicalEditor,
2549   domSelection: Selection,
2550   tags: Set<string>,
2551   rootElement: HTMLElement,
2552   nodeCount: number,
2553 ): void {
2554   const anchorDOMNode = domSelection.anchorNode;
2555   const focusDOMNode = domSelection.focusNode;
2556   const anchorOffset = domSelection.anchorOffset;
2557   const focusOffset = domSelection.focusOffset;
2558   const activeElement = document.activeElement;
2559
2560   // TODO: make this not hard-coded, and add another config option
2561   // that makes this configurable.
2562   if (
2563     (tags.has('collaboration') && activeElement !== rootElement) ||
2564     (activeElement !== null &&
2565       isSelectionCapturedInDecoratorInput(activeElement))
2566   ) {
2567     return;
2568   }
2569
2570   if (!$isRangeSelection(nextSelection)) {
2571     // We don't remove selection if the prevSelection is null because
2572     // of editor.setRootElement(). If this occurs on init when the
2573     // editor is already focused, then this can cause the editor to
2574     // lose focus.
2575     if (
2576       prevSelection !== null &&
2577       isSelectionWithinEditor(editor, anchorDOMNode, focusDOMNode)
2578     ) {
2579       domSelection.removeAllRanges();
2580     }
2581
2582     return;
2583   }
2584
2585   const anchor = nextSelection.anchor;
2586   const focus = nextSelection.focus;
2587   const anchorKey = anchor.key;
2588   const focusKey = focus.key;
2589   const anchorDOM = getElementByKeyOrThrow(editor, anchorKey);
2590   const focusDOM = getElementByKeyOrThrow(editor, focusKey);
2591   const nextAnchorOffset = anchor.offset;
2592   const nextFocusOffset = focus.offset;
2593   const nextFormat = nextSelection.format;
2594   const nextStyle = nextSelection.style;
2595   const isCollapsed = nextSelection.isCollapsed();
2596   let nextAnchorNode: HTMLElement | Text | null = anchorDOM;
2597   let nextFocusNode: HTMLElement | Text | null = focusDOM;
2598   let anchorFormatOrStyleChanged = false;
2599
2600   if (anchor.type === 'text') {
2601     nextAnchorNode = getDOMTextNode(anchorDOM);
2602     const anchorNode = anchor.getNode();
2603     anchorFormatOrStyleChanged =
2604       anchorNode.getFormat() !== nextFormat ||
2605       anchorNode.getStyle() !== nextStyle;
2606   } else if (
2607     $isRangeSelection(prevSelection) &&
2608     prevSelection.anchor.type === 'text'
2609   ) {
2610     anchorFormatOrStyleChanged = true;
2611   }
2612
2613   if (focus.type === 'text') {
2614     nextFocusNode = getDOMTextNode(focusDOM);
2615   }
2616
2617   // If we can't get an underlying text node for selection, then
2618   // we should avoid setting selection to something incorrect.
2619   if (nextAnchorNode === null || nextFocusNode === null) {
2620     return;
2621   }
2622
2623   if (
2624     isCollapsed &&
2625     (prevSelection === null ||
2626       anchorFormatOrStyleChanged ||
2627       ($isRangeSelection(prevSelection) &&
2628         (prevSelection.format !== nextFormat ||
2629           prevSelection.style !== nextStyle)))
2630   ) {
2631     markCollapsedSelectionFormat(
2632       nextFormat,
2633       nextStyle,
2634       nextAnchorOffset,
2635       anchorKey,
2636       performance.now(),
2637     );
2638   }
2639
2640   // Diff against the native DOM selection to ensure we don't do
2641   // an unnecessary selection update. We also skip this check if
2642   // we're moving selection to within an element, as this can
2643   // sometimes be problematic around scrolling.
2644   if (
2645     anchorOffset === nextAnchorOffset &&
2646     focusOffset === nextFocusOffset &&
2647     anchorDOMNode === nextAnchorNode &&
2648     focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
2649     !(domSelection.type === 'Range' && isCollapsed)
2650   ) {
2651     // If the root element does not have focus, ensure it has focus
2652     if (activeElement === null || !rootElement.contains(activeElement)) {
2653       rootElement.focus({
2654         preventScroll: true,
2655       });
2656     }
2657     if (anchor.type !== 'element') {
2658       return;
2659     }
2660   }
2661
2662   // Apply the updated selection to the DOM. Note: this will trigger
2663   // a "selectionchange" event, although it will be asynchronous.
2664   try {
2665     domSelection.setBaseAndExtent(
2666       nextAnchorNode,
2667       nextAnchorOffset,
2668       nextFocusNode,
2669       nextFocusOffset,
2670     );
2671   } catch (error) {
2672     // If we encounter an error, continue. This can sometimes
2673     // occur with FF and there's no good reason as to why it
2674     // should happen.
2675     if (__DEV__) {
2676       console.warn(error);
2677     }
2678   }
2679   if (
2680     !tags.has('skip-scroll-into-view') &&
2681     nextSelection.isCollapsed() &&
2682     rootElement !== null &&
2683     rootElement === document.activeElement
2684   ) {
2685     const selectionTarget: null | Range | HTMLElement | Text =
2686       nextSelection instanceof RangeSelection &&
2687       nextSelection.anchor.type === 'element'
2688         ? (nextAnchorNode.childNodes[nextAnchorOffset] as HTMLElement | Text) ||
2689           null
2690         : domSelection.rangeCount > 0
2691         ? domSelection.getRangeAt(0)
2692         : null;
2693     if (selectionTarget !== null) {
2694       let selectionRect: DOMRect;
2695       if (selectionTarget instanceof Text) {
2696         const range = document.createRange();
2697         range.selectNode(selectionTarget);
2698         selectionRect = range.getBoundingClientRect();
2699       } else {
2700         selectionRect = selectionTarget.getBoundingClientRect();
2701       }
2702       scrollIntoViewIfNeeded(editor, selectionRect, rootElement);
2703     }
2704   }
2705
2706   markSelectionChangeFromDOMUpdate();
2707 }
2708
2709 export function $insertNodes(nodes: Array<LexicalNode>) {
2710   let selection = $getSelection() || $getPreviousSelection();
2711
2712   if (selection === null) {
2713     selection = $getRoot().selectEnd();
2714   }
2715   selection.insertNodes(nodes);
2716 }
2717
2718 export function $getTextContent(): string {
2719   const selection = $getSelection();
2720   if (selection === null) {
2721     return '';
2722   }
2723   return selection.getTextContent();
2724 }
2725
2726 function $removeTextAndSplitBlock(selection: RangeSelection): number {
2727   let selection_ = selection;
2728   if (!selection.isCollapsed()) {
2729     selection_.removeText();
2730   }
2731   // A new selection can originate as a result of node replacement, in which case is registered via
2732   // $setSelection
2733   const newSelection = $getSelection();
2734   if ($isRangeSelection(newSelection)) {
2735     selection_ = newSelection;
2736   }
2737
2738   invariant(
2739     $isRangeSelection(selection_),
2740     'Unexpected dirty selection to be null',
2741   );
2742
2743   const anchor = selection_.anchor;
2744   let node = anchor.getNode();
2745   let offset = anchor.offset;
2746
2747   while (!INTERNAL_$isBlock(node)) {
2748     [node, offset] = $splitNodeAtPoint(node, offset);
2749   }
2750
2751   return offset;
2752 }
2753
2754 function $splitNodeAtPoint(
2755   node: LexicalNode,
2756   offset: number,
2757 ): [parent: ElementNode, offset: number] {
2758   const parent = node.getParent();
2759   if (!parent) {
2760     const paragraph = $createParagraphNode();
2761     $getRoot().append(paragraph);
2762     paragraph.select();
2763     return [$getRoot(), 0];
2764   }
2765
2766   if ($isTextNode(node)) {
2767     const split = node.splitText(offset);
2768     if (split.length === 0) {
2769       return [parent, node.getIndexWithinParent()];
2770     }
2771     const x = offset === 0 ? 0 : 1;
2772     const index = split[0].getIndexWithinParent() + x;
2773
2774     return [parent, index];
2775   }
2776
2777   if (!$isElementNode(node) || offset === 0) {
2778     return [parent, node.getIndexWithinParent()];
2779   }
2780
2781   const firstToAppend = node.getChildAtIndex(offset);
2782   if (firstToAppend) {
2783     const insertPoint = new RangeSelection(
2784       $createPoint(node.__key, offset, 'element'),
2785       $createPoint(node.__key, offset, 'element'),
2786       0,
2787       '',
2788     );
2789     const newElement = node.insertNewAfter(insertPoint) as ElementNode | null;
2790     if (newElement) {
2791       newElement.append(firstToAppend, ...firstToAppend.getNextSiblings());
2792     }
2793   }
2794   return [parent, node.getIndexWithinParent() + 1];
2795 }
2796
2797 function $wrapInlineNodes(nodes: LexicalNode[]) {
2798   // We temporarily insert the topLevelNodes into an arbitrary ElementNode,
2799   // since insertAfter does not work on nodes that have no parent (TO-DO: fix that).
2800   const virtualRoot = $createParagraphNode();
2801
2802   let currentBlock = null;
2803   for (let i = 0; i < nodes.length; i++) {
2804     const node = nodes[i];
2805
2806     const isLineBreakNode = $isLineBreakNode(node);
2807
2808     if (
2809       isLineBreakNode ||
2810       ($isDecoratorNode(node) && node.isInline()) ||
2811       ($isElementNode(node) && node.isInline()) ||
2812       $isTextNode(node) ||
2813       node.isParentRequired()
2814     ) {
2815       if (currentBlock === null) {
2816         currentBlock = node.createParentElementNode();
2817         virtualRoot.append(currentBlock);
2818         // In the case of LineBreakNode, we just need to
2819         // add an empty ParagraphNode to the topLevelBlocks.
2820         if (isLineBreakNode) {
2821           continue;
2822         }
2823       }
2824
2825       if (currentBlock !== null) {
2826         currentBlock.append(node);
2827       }
2828     } else {
2829       virtualRoot.append(node);
2830       currentBlock = null;
2831     }
2832   }
2833
2834   return virtualRoot;
2835 }