]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/index.ts
Lexical: Imported core lexical libs
[bookstack] / resources / js / wysiwyg / lexical / rich-text / index.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {
10   CommandPayloadType,
11   DOMConversionMap,
12   DOMConversionOutput,
13   DOMExportOutput,
14   EditorConfig,
15   ElementFormatType,
16   LexicalCommand,
17   LexicalEditor,
18   LexicalNode,
19   NodeKey,
20   ParagraphNode,
21   PasteCommandType,
22   RangeSelection,
23   SerializedElementNode,
24   Spread,
25   TextFormatType,
26 } from 'lexical';
27
28 import {
29   $insertDataTransferForRichText,
30   copyToClipboard,
31 } from '@lexical/clipboard';
32 import {
33   $moveCharacter,
34   $shouldOverrideDefaultCharacterSelection,
35 } from '@lexical/selection';
36 import {
37   $findMatchingParent,
38   $getNearestBlockElementAncestorOrThrow,
39   addClassNamesToElement,
40   isHTMLElement,
41   mergeRegister,
42   objectKlassEquals,
43 } from '@lexical/utils';
44 import {
45   $applyNodeReplacement,
46   $createParagraphNode,
47   $createRangeSelection,
48   $createTabNode,
49   $getAdjacentNode,
50   $getNearestNodeFromDOMNode,
51   $getRoot,
52   $getSelection,
53   $insertNodes,
54   $isDecoratorNode,
55   $isElementNode,
56   $isNodeSelection,
57   $isRangeSelection,
58   $isRootNode,
59   $isTextNode,
60   $normalizeSelection__EXPERIMENTAL,
61   $selectAll,
62   $setSelection,
63   CLICK_COMMAND,
64   COMMAND_PRIORITY_EDITOR,
65   CONTROLLED_TEXT_INSERTION_COMMAND,
66   COPY_COMMAND,
67   createCommand,
68   CUT_COMMAND,
69   DELETE_CHARACTER_COMMAND,
70   DELETE_LINE_COMMAND,
71   DELETE_WORD_COMMAND,
72   DRAGOVER_COMMAND,
73   DRAGSTART_COMMAND,
74   DROP_COMMAND,
75   ElementNode,
76   FORMAT_ELEMENT_COMMAND,
77   FORMAT_TEXT_COMMAND,
78   INDENT_CONTENT_COMMAND,
79   INSERT_LINE_BREAK_COMMAND,
80   INSERT_PARAGRAPH_COMMAND,
81   INSERT_TAB_COMMAND,
82   isSelectionCapturedInDecoratorInput,
83   KEY_ARROW_DOWN_COMMAND,
84   KEY_ARROW_LEFT_COMMAND,
85   KEY_ARROW_RIGHT_COMMAND,
86   KEY_ARROW_UP_COMMAND,
87   KEY_BACKSPACE_COMMAND,
88   KEY_DELETE_COMMAND,
89   KEY_ENTER_COMMAND,
90   KEY_ESCAPE_COMMAND,
91   OUTDENT_CONTENT_COMMAND,
92   PASTE_COMMAND,
93   REMOVE_TEXT_COMMAND,
94   SELECT_ALL_COMMAND,
95 } from 'lexical';
96 import caretFromPoint from 'lexical/shared/caretFromPoint';
97 import {
98   CAN_USE_BEFORE_INPUT,
99   IS_APPLE_WEBKIT,
100   IS_IOS,
101   IS_SAFARI,
102 } from 'lexical/shared/environment';
103
104 export type SerializedHeadingNode = Spread<
105   {
106     tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
107   },
108   SerializedElementNode
109 >;
110
111 export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
112   'DRAG_DROP_PASTE_FILE',
113 );
114
115 export type SerializedQuoteNode = SerializedElementNode;
116
117 /** @noInheritDoc */
118 export class QuoteNode extends ElementNode {
119   static getType(): string {
120     return 'quote';
121   }
122
123   static clone(node: QuoteNode): QuoteNode {
124     return new QuoteNode(node.__key);
125   }
126
127   constructor(key?: NodeKey) {
128     super(key);
129   }
130
131   // View
132
133   createDOM(config: EditorConfig): HTMLElement {
134     const element = document.createElement('blockquote');
135     addClassNamesToElement(element, config.theme.quote);
136     return element;
137   }
138   updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
139     return false;
140   }
141
142   static importDOM(): DOMConversionMap | null {
143     return {
144       blockquote: (node: Node) => ({
145         conversion: $convertBlockquoteElement,
146         priority: 0,
147       }),
148     };
149   }
150
151   exportDOM(editor: LexicalEditor): DOMExportOutput {
152     const {element} = super.exportDOM(editor);
153
154     if (element && isHTMLElement(element)) {
155       if (this.isEmpty()) {
156         element.append(document.createElement('br'));
157       }
158
159       const formatType = this.getFormatType();
160       element.style.textAlign = formatType;
161
162       const direction = this.getDirection();
163       if (direction) {
164         element.dir = direction;
165       }
166     }
167
168     return {
169       element,
170     };
171   }
172
173   static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
174     const node = $createQuoteNode();
175     node.setFormat(serializedNode.format);
176     node.setIndent(serializedNode.indent);
177     node.setDirection(serializedNode.direction);
178     return node;
179   }
180
181   exportJSON(): SerializedElementNode {
182     return {
183       ...super.exportJSON(),
184       type: 'quote',
185     };
186   }
187
188   // Mutation
189
190   insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
191     const newBlock = $createParagraphNode();
192     const direction = this.getDirection();
193     newBlock.setDirection(direction);
194     this.insertAfter(newBlock, restoreSelection);
195     return newBlock;
196   }
197
198   collapseAtStart(): true {
199     const paragraph = $createParagraphNode();
200     const children = this.getChildren();
201     children.forEach((child) => paragraph.append(child));
202     this.replace(paragraph);
203     return true;
204   }
205
206   canMergeWhenEmpty(): true {
207     return true;
208   }
209 }
210
211 export function $createQuoteNode(): QuoteNode {
212   return $applyNodeReplacement(new QuoteNode());
213 }
214
215 export function $isQuoteNode(
216   node: LexicalNode | null | undefined,
217 ): node is QuoteNode {
218   return node instanceof QuoteNode;
219 }
220
221 export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
222
223 /** @noInheritDoc */
224 export class HeadingNode extends ElementNode {
225   /** @internal */
226   __tag: HeadingTagType;
227
228   static getType(): string {
229     return 'heading';
230   }
231
232   static clone(node: HeadingNode): HeadingNode {
233     return new HeadingNode(node.__tag, node.__key);
234   }
235
236   constructor(tag: HeadingTagType, key?: NodeKey) {
237     super(key);
238     this.__tag = tag;
239   }
240
241   getTag(): HeadingTagType {
242     return this.__tag;
243   }
244
245   // View
246
247   createDOM(config: EditorConfig): HTMLElement {
248     const tag = this.__tag;
249     const element = document.createElement(tag);
250     const theme = config.theme;
251     const classNames = theme.heading;
252     if (classNames !== undefined) {
253       const className = classNames[tag];
254       addClassNamesToElement(element, className);
255     }
256     return element;
257   }
258
259   updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
260     return false;
261   }
262
263   static importDOM(): DOMConversionMap | null {
264     return {
265       h1: (node: Node) => ({
266         conversion: $convertHeadingElement,
267         priority: 0,
268       }),
269       h2: (node: Node) => ({
270         conversion: $convertHeadingElement,
271         priority: 0,
272       }),
273       h3: (node: Node) => ({
274         conversion: $convertHeadingElement,
275         priority: 0,
276       }),
277       h4: (node: Node) => ({
278         conversion: $convertHeadingElement,
279         priority: 0,
280       }),
281       h5: (node: Node) => ({
282         conversion: $convertHeadingElement,
283         priority: 0,
284       }),
285       h6: (node: Node) => ({
286         conversion: $convertHeadingElement,
287         priority: 0,
288       }),
289       p: (node: Node) => {
290         // domNode is a <p> since we matched it by nodeName
291         const paragraph = node as HTMLParagraphElement;
292         const firstChild = paragraph.firstChild;
293         if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
294           return {
295             conversion: () => ({node: null}),
296             priority: 3,
297           };
298         }
299         return null;
300       },
301       span: (node: Node) => {
302         if (isGoogleDocsTitle(node)) {
303           return {
304             conversion: (domNode: Node) => {
305               return {
306                 node: $createHeadingNode('h1'),
307               };
308             },
309             priority: 3,
310           };
311         }
312         return null;
313       },
314     };
315   }
316
317   exportDOM(editor: LexicalEditor): DOMExportOutput {
318     const {element} = super.exportDOM(editor);
319
320     if (element && isHTMLElement(element)) {
321       if (this.isEmpty()) {
322         element.append(document.createElement('br'));
323       }
324
325       const formatType = this.getFormatType();
326       element.style.textAlign = formatType;
327
328       const direction = this.getDirection();
329       if (direction) {
330         element.dir = direction;
331       }
332     }
333
334     return {
335       element,
336     };
337   }
338
339   static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
340     const node = $createHeadingNode(serializedNode.tag);
341     node.setFormat(serializedNode.format);
342     node.setIndent(serializedNode.indent);
343     node.setDirection(serializedNode.direction);
344     return node;
345   }
346
347   exportJSON(): SerializedHeadingNode {
348     return {
349       ...super.exportJSON(),
350       tag: this.getTag(),
351       type: 'heading',
352       version: 1,
353     };
354   }
355
356   // Mutation
357   insertNewAfter(
358     selection?: RangeSelection,
359     restoreSelection = true,
360   ): ParagraphNode | HeadingNode {
361     const anchorOffet = selection ? selection.anchor.offset : 0;
362     const lastDesc = this.getLastDescendant();
363     const isAtEnd =
364       !lastDesc ||
365       (selection &&
366         selection.anchor.key === lastDesc.getKey() &&
367         anchorOffet === lastDesc.getTextContentSize());
368     const newElement =
369       isAtEnd || !selection
370         ? $createParagraphNode()
371         : $createHeadingNode(this.getTag());
372     const direction = this.getDirection();
373     newElement.setDirection(direction);
374     this.insertAfter(newElement, restoreSelection);
375     if (anchorOffet === 0 && !this.isEmpty() && selection) {
376       const paragraph = $createParagraphNode();
377       paragraph.select();
378       this.replace(paragraph, true);
379     }
380     return newElement;
381   }
382
383   collapseAtStart(): true {
384     const newElement = !this.isEmpty()
385       ? $createHeadingNode(this.getTag())
386       : $createParagraphNode();
387     const children = this.getChildren();
388     children.forEach((child) => newElement.append(child));
389     this.replace(newElement);
390     return true;
391   }
392
393   extractWithChild(): boolean {
394     return true;
395   }
396 }
397
398 function isGoogleDocsTitle(domNode: Node): boolean {
399   if (domNode.nodeName.toLowerCase() === 'span') {
400     return (domNode as HTMLSpanElement).style.fontSize === '26pt';
401   }
402   return false;
403 }
404
405 function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
406   const nodeName = element.nodeName.toLowerCase();
407   let node = null;
408   if (
409     nodeName === 'h1' ||
410     nodeName === 'h2' ||
411     nodeName === 'h3' ||
412     nodeName === 'h4' ||
413     nodeName === 'h5' ||
414     nodeName === 'h6'
415   ) {
416     node = $createHeadingNode(nodeName);
417     if (element.style !== null) {
418       node.setFormat(element.style.textAlign as ElementFormatType);
419     }
420   }
421   return {node};
422 }
423
424 function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
425   const node = $createQuoteNode();
426   if (element.style !== null) {
427     node.setFormat(element.style.textAlign as ElementFormatType);
428   }
429   return {node};
430 }
431
432 export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
433   return $applyNodeReplacement(new HeadingNode(headingTag));
434 }
435
436 export function $isHeadingNode(
437   node: LexicalNode | null | undefined,
438 ): node is HeadingNode {
439   return node instanceof HeadingNode;
440 }
441
442 function onPasteForRichText(
443   event: CommandPayloadType<typeof PASTE_COMMAND>,
444   editor: LexicalEditor,
445 ): void {
446   event.preventDefault();
447   editor.update(
448     () => {
449       const selection = $getSelection();
450       const clipboardData =
451         objectKlassEquals(event, InputEvent) ||
452         objectKlassEquals(event, KeyboardEvent)
453           ? null
454           : (event as ClipboardEvent).clipboardData;
455       if (clipboardData != null && selection !== null) {
456         $insertDataTransferForRichText(clipboardData, selection, editor);
457       }
458     },
459     {
460       tag: 'paste',
461     },
462   );
463 }
464
465 async function onCutForRichText(
466   event: CommandPayloadType<typeof CUT_COMMAND>,
467   editor: LexicalEditor,
468 ): Promise<void> {
469   await copyToClipboard(
470     editor,
471     objectKlassEquals(event, ClipboardEvent) ? (event as ClipboardEvent) : null,
472   );
473   editor.update(() => {
474     const selection = $getSelection();
475     if ($isRangeSelection(selection)) {
476       selection.removeText();
477     } else if ($isNodeSelection(selection)) {
478       selection.getNodes().forEach((node) => node.remove());
479     }
480   });
481 }
482
483 // Clipboard may contain files that we aren't allowed to read. While the event is arguably useless,
484 // in certain occasions, we want to know whether it was a file transfer, as opposed to text. We
485 // control this with the first boolean flag.
486 export function eventFiles(
487   event: DragEvent | PasteCommandType,
488 ): [boolean, Array<File>, boolean] {
489   let dataTransfer: null | DataTransfer = null;
490   if (objectKlassEquals(event, DragEvent)) {
491     dataTransfer = (event as DragEvent).dataTransfer;
492   } else if (objectKlassEquals(event, ClipboardEvent)) {
493     dataTransfer = (event as ClipboardEvent).clipboardData;
494   }
495
496   if (dataTransfer === null) {
497     return [false, [], false];
498   }
499
500   const types = dataTransfer.types;
501   const hasFiles = types.includes('Files');
502   const hasContent =
503     types.includes('text/html') || types.includes('text/plain');
504   return [hasFiles, Array.from(dataTransfer.files), hasContent];
505 }
506
507 function $handleIndentAndOutdent(
508   indentOrOutdent: (block: ElementNode) => void,
509 ): boolean {
510   const selection = $getSelection();
511   if (!$isRangeSelection(selection)) {
512     return false;
513   }
514   const alreadyHandled = new Set();
515   const nodes = selection.getNodes();
516   for (let i = 0; i < nodes.length; i++) {
517     const node = nodes[i];
518     const key = node.getKey();
519     if (alreadyHandled.has(key)) {
520       continue;
521     }
522     const parentBlock = $findMatchingParent(
523       node,
524       (parentNode): parentNode is ElementNode =>
525         $isElementNode(parentNode) && !parentNode.isInline(),
526     );
527     if (parentBlock === null) {
528       continue;
529     }
530     const parentKey = parentBlock.getKey();
531     if (parentBlock.canIndent() && !alreadyHandled.has(parentKey)) {
532       alreadyHandled.add(parentKey);
533       indentOrOutdent(parentBlock);
534     }
535   }
536   return alreadyHandled.size > 0;
537 }
538
539 function $isTargetWithinDecorator(target: HTMLElement): boolean {
540   const node = $getNearestNodeFromDOMNode(target);
541   return $isDecoratorNode(node);
542 }
543
544 function $isSelectionAtEndOfRoot(selection: RangeSelection) {
545   const focus = selection.focus;
546   return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize();
547 }
548
549 export function registerRichText(editor: LexicalEditor): () => void {
550   const removeListener = mergeRegister(
551     editor.registerCommand(
552       CLICK_COMMAND,
553       (payload) => {
554         const selection = $getSelection();
555         if ($isNodeSelection(selection)) {
556           selection.clear();
557           return true;
558         }
559         return false;
560       },
561       0,
562     ),
563     editor.registerCommand<boolean>(
564       DELETE_CHARACTER_COMMAND,
565       (isBackward) => {
566         const selection = $getSelection();
567         if (!$isRangeSelection(selection)) {
568           return false;
569         }
570         selection.deleteCharacter(isBackward);
571         return true;
572       },
573       COMMAND_PRIORITY_EDITOR,
574     ),
575     editor.registerCommand<boolean>(
576       DELETE_WORD_COMMAND,
577       (isBackward) => {
578         const selection = $getSelection();
579         if (!$isRangeSelection(selection)) {
580           return false;
581         }
582         selection.deleteWord(isBackward);
583         return true;
584       },
585       COMMAND_PRIORITY_EDITOR,
586     ),
587     editor.registerCommand<boolean>(
588       DELETE_LINE_COMMAND,
589       (isBackward) => {
590         const selection = $getSelection();
591         if (!$isRangeSelection(selection)) {
592           return false;
593         }
594         selection.deleteLine(isBackward);
595         return true;
596       },
597       COMMAND_PRIORITY_EDITOR,
598     ),
599     editor.registerCommand(
600       CONTROLLED_TEXT_INSERTION_COMMAND,
601       (eventOrText) => {
602         const selection = $getSelection();
603
604         if (typeof eventOrText === 'string') {
605           if (selection !== null) {
606             selection.insertText(eventOrText);
607           }
608         } else {
609           if (selection === null) {
610             return false;
611           }
612
613           const dataTransfer = eventOrText.dataTransfer;
614           if (dataTransfer != null) {
615             $insertDataTransferForRichText(dataTransfer, selection, editor);
616           } else if ($isRangeSelection(selection)) {
617             const data = eventOrText.data;
618             if (data) {
619               selection.insertText(data);
620             }
621             return true;
622           }
623         }
624         return true;
625       },
626       COMMAND_PRIORITY_EDITOR,
627     ),
628     editor.registerCommand(
629       REMOVE_TEXT_COMMAND,
630       () => {
631         const selection = $getSelection();
632         if (!$isRangeSelection(selection)) {
633           return false;
634         }
635         selection.removeText();
636         return true;
637       },
638       COMMAND_PRIORITY_EDITOR,
639     ),
640     editor.registerCommand<TextFormatType>(
641       FORMAT_TEXT_COMMAND,
642       (format) => {
643         const selection = $getSelection();
644         if (!$isRangeSelection(selection)) {
645           return false;
646         }
647         selection.formatText(format);
648         return true;
649       },
650       COMMAND_PRIORITY_EDITOR,
651     ),
652     editor.registerCommand<ElementFormatType>(
653       FORMAT_ELEMENT_COMMAND,
654       (format) => {
655         const selection = $getSelection();
656         if (!$isRangeSelection(selection) && !$isNodeSelection(selection)) {
657           return false;
658         }
659         const nodes = selection.getNodes();
660         for (const node of nodes) {
661           const element = $findMatchingParent(
662             node,
663             (parentNode): parentNode is ElementNode =>
664               $isElementNode(parentNode) && !parentNode.isInline(),
665           );
666           if (element !== null) {
667             element.setFormat(format);
668           }
669         }
670         return true;
671       },
672       COMMAND_PRIORITY_EDITOR,
673     ),
674     editor.registerCommand<boolean>(
675       INSERT_LINE_BREAK_COMMAND,
676       (selectStart) => {
677         const selection = $getSelection();
678         if (!$isRangeSelection(selection)) {
679           return false;
680         }
681         selection.insertLineBreak(selectStart);
682         return true;
683       },
684       COMMAND_PRIORITY_EDITOR,
685     ),
686     editor.registerCommand(
687       INSERT_PARAGRAPH_COMMAND,
688       () => {
689         const selection = $getSelection();
690         if (!$isRangeSelection(selection)) {
691           return false;
692         }
693         selection.insertParagraph();
694         return true;
695       },
696       COMMAND_PRIORITY_EDITOR,
697     ),
698     editor.registerCommand(
699       INSERT_TAB_COMMAND,
700       () => {
701         $insertNodes([$createTabNode()]);
702         return true;
703       },
704       COMMAND_PRIORITY_EDITOR,
705     ),
706     editor.registerCommand(
707       INDENT_CONTENT_COMMAND,
708       () => {
709         return $handleIndentAndOutdent((block) => {
710           const indent = block.getIndent();
711           block.setIndent(indent + 1);
712         });
713       },
714       COMMAND_PRIORITY_EDITOR,
715     ),
716     editor.registerCommand(
717       OUTDENT_CONTENT_COMMAND,
718       () => {
719         return $handleIndentAndOutdent((block) => {
720           const indent = block.getIndent();
721           if (indent > 0) {
722             block.setIndent(indent - 1);
723           }
724         });
725       },
726       COMMAND_PRIORITY_EDITOR,
727     ),
728     editor.registerCommand<KeyboardEvent>(
729       KEY_ARROW_UP_COMMAND,
730       (event) => {
731         const selection = $getSelection();
732         if (
733           $isNodeSelection(selection) &&
734           !$isTargetWithinDecorator(event.target as HTMLElement)
735         ) {
736           // If selection is on a node, let's try and move selection
737           // back to being a range selection.
738           const nodes = selection.getNodes();
739           if (nodes.length > 0) {
740             nodes[0].selectPrevious();
741             return true;
742           }
743         } else if ($isRangeSelection(selection)) {
744           const possibleNode = $getAdjacentNode(selection.focus, true);
745           if (
746             !event.shiftKey &&
747             $isDecoratorNode(possibleNode) &&
748             !possibleNode.isIsolated() &&
749             !possibleNode.isInline()
750           ) {
751             possibleNode.selectPrevious();
752             event.preventDefault();
753             return true;
754           }
755         }
756         return false;
757       },
758       COMMAND_PRIORITY_EDITOR,
759     ),
760     editor.registerCommand<KeyboardEvent>(
761       KEY_ARROW_DOWN_COMMAND,
762       (event) => {
763         const selection = $getSelection();
764         if ($isNodeSelection(selection)) {
765           // If selection is on a node, let's try and move selection
766           // back to being a range selection.
767           const nodes = selection.getNodes();
768           if (nodes.length > 0) {
769             nodes[0].selectNext(0, 0);
770             return true;
771           }
772         } else if ($isRangeSelection(selection)) {
773           if ($isSelectionAtEndOfRoot(selection)) {
774             event.preventDefault();
775             return true;
776           }
777           const possibleNode = $getAdjacentNode(selection.focus, false);
778           if (
779             !event.shiftKey &&
780             $isDecoratorNode(possibleNode) &&
781             !possibleNode.isIsolated() &&
782             !possibleNode.isInline()
783           ) {
784             possibleNode.selectNext();
785             event.preventDefault();
786             return true;
787           }
788         }
789         return false;
790       },
791       COMMAND_PRIORITY_EDITOR,
792     ),
793     editor.registerCommand<KeyboardEvent>(
794       KEY_ARROW_LEFT_COMMAND,
795       (event) => {
796         const selection = $getSelection();
797         if ($isNodeSelection(selection)) {
798           // If selection is on a node, let's try and move selection
799           // back to being a range selection.
800           const nodes = selection.getNodes();
801           if (nodes.length > 0) {
802             event.preventDefault();
803             nodes[0].selectPrevious();
804             return true;
805           }
806         }
807         if (!$isRangeSelection(selection)) {
808           return false;
809         }
810         if ($shouldOverrideDefaultCharacterSelection(selection, true)) {
811           const isHoldingShift = event.shiftKey;
812           event.preventDefault();
813           $moveCharacter(selection, isHoldingShift, true);
814           return true;
815         }
816         return false;
817       },
818       COMMAND_PRIORITY_EDITOR,
819     ),
820     editor.registerCommand<KeyboardEvent>(
821       KEY_ARROW_RIGHT_COMMAND,
822       (event) => {
823         const selection = $getSelection();
824         if (
825           $isNodeSelection(selection) &&
826           !$isTargetWithinDecorator(event.target as HTMLElement)
827         ) {
828           // If selection is on a node, let's try and move selection
829           // back to being a range selection.
830           const nodes = selection.getNodes();
831           if (nodes.length > 0) {
832             event.preventDefault();
833             nodes[0].selectNext(0, 0);
834             return true;
835           }
836         }
837         if (!$isRangeSelection(selection)) {
838           return false;
839         }
840         const isHoldingShift = event.shiftKey;
841         if ($shouldOverrideDefaultCharacterSelection(selection, false)) {
842           event.preventDefault();
843           $moveCharacter(selection, isHoldingShift, false);
844           return true;
845         }
846         return false;
847       },
848       COMMAND_PRIORITY_EDITOR,
849     ),
850     editor.registerCommand<KeyboardEvent>(
851       KEY_BACKSPACE_COMMAND,
852       (event) => {
853         if ($isTargetWithinDecorator(event.target as HTMLElement)) {
854           return false;
855         }
856         const selection = $getSelection();
857         if (!$isRangeSelection(selection)) {
858           return false;
859         }
860         event.preventDefault();
861         const {anchor} = selection;
862         const anchorNode = anchor.getNode();
863
864         if (
865           selection.isCollapsed() &&
866           anchor.offset === 0 &&
867           !$isRootNode(anchorNode)
868         ) {
869           const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
870           if (element.getIndent() > 0) {
871             return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
872           }
873         }
874         return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
875       },
876       COMMAND_PRIORITY_EDITOR,
877     ),
878     editor.registerCommand<KeyboardEvent>(
879       KEY_DELETE_COMMAND,
880       (event) => {
881         if ($isTargetWithinDecorator(event.target as HTMLElement)) {
882           return false;
883         }
884         const selection = $getSelection();
885         if (!$isRangeSelection(selection)) {
886           return false;
887         }
888         event.preventDefault();
889         return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, false);
890       },
891       COMMAND_PRIORITY_EDITOR,
892     ),
893     editor.registerCommand<KeyboardEvent | null>(
894       KEY_ENTER_COMMAND,
895       (event) => {
896         const selection = $getSelection();
897         if (!$isRangeSelection(selection)) {
898           return false;
899         }
900         if (event !== null) {
901           // If we have beforeinput, then we can avoid blocking
902           // the default behavior. This ensures that the iOS can
903           // intercept that we're actually inserting a paragraph,
904           // and autocomplete, autocapitalize etc work as intended.
905           // This can also cause a strange performance issue in
906           // Safari, where there is a noticeable pause due to
907           // preventing the key down of enter.
908           if (
909             (IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&
910             CAN_USE_BEFORE_INPUT
911           ) {
912             return false;
913           }
914           event.preventDefault();
915           if (event.shiftKey) {
916             return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);
917           }
918         }
919         return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
920       },
921       COMMAND_PRIORITY_EDITOR,
922     ),
923     editor.registerCommand(
924       KEY_ESCAPE_COMMAND,
925       () => {
926         const selection = $getSelection();
927         if (!$isRangeSelection(selection)) {
928           return false;
929         }
930         editor.blur();
931         return true;
932       },
933       COMMAND_PRIORITY_EDITOR,
934     ),
935     editor.registerCommand<DragEvent>(
936       DROP_COMMAND,
937       (event) => {
938         const [, files] = eventFiles(event);
939         if (files.length > 0) {
940           const x = event.clientX;
941           const y = event.clientY;
942           const eventRange = caretFromPoint(x, y);
943           if (eventRange !== null) {
944             const {offset: domOffset, node: domNode} = eventRange;
945             const node = $getNearestNodeFromDOMNode(domNode);
946             if (node !== null) {
947               const selection = $createRangeSelection();
948               if ($isTextNode(node)) {
949                 selection.anchor.set(node.getKey(), domOffset, 'text');
950                 selection.focus.set(node.getKey(), domOffset, 'text');
951               } else {
952                 const parentKey = node.getParentOrThrow().getKey();
953                 const offset = node.getIndexWithinParent() + 1;
954                 selection.anchor.set(parentKey, offset, 'element');
955                 selection.focus.set(parentKey, offset, 'element');
956               }
957               const normalizedSelection =
958                 $normalizeSelection__EXPERIMENTAL(selection);
959               $setSelection(normalizedSelection);
960             }
961             editor.dispatchCommand(DRAG_DROP_PASTE, files);
962           }
963           event.preventDefault();
964           return true;
965         }
966
967         const selection = $getSelection();
968         if ($isRangeSelection(selection)) {
969           return true;
970         }
971
972         return false;
973       },
974       COMMAND_PRIORITY_EDITOR,
975     ),
976     editor.registerCommand<DragEvent>(
977       DRAGSTART_COMMAND,
978       (event) => {
979         const [isFileTransfer] = eventFiles(event);
980         const selection = $getSelection();
981         if (isFileTransfer && !$isRangeSelection(selection)) {
982           return false;
983         }
984         return true;
985       },
986       COMMAND_PRIORITY_EDITOR,
987     ),
988     editor.registerCommand<DragEvent>(
989       DRAGOVER_COMMAND,
990       (event) => {
991         const [isFileTransfer] = eventFiles(event);
992         const selection = $getSelection();
993         if (isFileTransfer && !$isRangeSelection(selection)) {
994           return false;
995         }
996         const x = event.clientX;
997         const y = event.clientY;
998         const eventRange = caretFromPoint(x, y);
999         if (eventRange !== null) {
1000           const node = $getNearestNodeFromDOMNode(eventRange.node);
1001           if ($isDecoratorNode(node)) {
1002             // Show browser caret as the user is dragging the media across the screen. Won't work
1003             // for DecoratorNode nor it's relevant.
1004             event.preventDefault();
1005           }
1006         }
1007         return true;
1008       },
1009       COMMAND_PRIORITY_EDITOR,
1010     ),
1011     editor.registerCommand(
1012       SELECT_ALL_COMMAND,
1013       () => {
1014         $selectAll();
1015
1016         return true;
1017       },
1018       COMMAND_PRIORITY_EDITOR,
1019     ),
1020     editor.registerCommand(
1021       COPY_COMMAND,
1022       (event) => {
1023         copyToClipboard(
1024           editor,
1025           objectKlassEquals(event, ClipboardEvent)
1026             ? (event as ClipboardEvent)
1027             : null,
1028         );
1029         return true;
1030       },
1031       COMMAND_PRIORITY_EDITOR,
1032     ),
1033     editor.registerCommand(
1034       CUT_COMMAND,
1035       (event) => {
1036         onCutForRichText(event, editor);
1037         return true;
1038       },
1039       COMMAND_PRIORITY_EDITOR,
1040     ),
1041     editor.registerCommand(
1042       PASTE_COMMAND,
1043       (event) => {
1044         const [, files, hasTextContent] = eventFiles(event);
1045         if (files.length > 0 && !hasTextContent) {
1046           editor.dispatchCommand(DRAG_DROP_PASTE, files);
1047           return true;
1048         }
1049
1050         // if inputs then paste within the input ignore creating a new node on paste event
1051         if (isSelectionCapturedInDecoratorInput(event.target as Node)) {
1052           return false;
1053         }
1054
1055         const selection = $getSelection();
1056         if (selection !== null) {
1057           onPasteForRichText(event, editor);
1058           return true;
1059         }
1060
1061         return false;
1062       },
1063       COMMAND_PRIORITY_EDITOR,
1064     ),
1065   );
1066   return removeListener;
1067 }