1056 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			1056 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
/**
 | 
						|
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 | 
						|
 *
 | 
						|
 * This source code is licensed under the MIT license found in the
 | 
						|
 * LICENSE file in the root directory of this source tree.
 | 
						|
 *
 | 
						|
 */
 | 
						|
 | 
						|
import type {
 | 
						|
  CommandPayloadType,
 | 
						|
  DOMConversionMap,
 | 
						|
  DOMConversionOutput,
 | 
						|
  DOMExportOutput,
 | 
						|
  EditorConfig,
 | 
						|
  ElementFormatType,
 | 
						|
  LexicalCommand,
 | 
						|
  LexicalEditor,
 | 
						|
  LexicalNode,
 | 
						|
  NodeKey,
 | 
						|
  ParagraphNode,
 | 
						|
  PasteCommandType,
 | 
						|
  RangeSelection,
 | 
						|
  SerializedElementNode,
 | 
						|
  Spread,
 | 
						|
  TextFormatType,
 | 
						|
} from 'lexical';
 | 
						|
 | 
						|
import {
 | 
						|
  $insertDataTransferForRichText,
 | 
						|
  copyToClipboard,
 | 
						|
} from '@lexical/clipboard';
 | 
						|
import {
 | 
						|
  $moveCharacter,
 | 
						|
  $shouldOverrideDefaultCharacterSelection,
 | 
						|
} from '@lexical/selection';
 | 
						|
import {
 | 
						|
  $findMatchingParent,
 | 
						|
  $getNearestBlockElementAncestorOrThrow,
 | 
						|
  addClassNamesToElement,
 | 
						|
  isHTMLElement,
 | 
						|
  mergeRegister,
 | 
						|
  objectKlassEquals,
 | 
						|
} from '@lexical/utils';
 | 
						|
import {
 | 
						|
  $applyNodeReplacement,
 | 
						|
  $createParagraphNode,
 | 
						|
  $createRangeSelection,
 | 
						|
  $createTabNode,
 | 
						|
  $getAdjacentNode,
 | 
						|
  $getNearestNodeFromDOMNode,
 | 
						|
  $getRoot,
 | 
						|
  $getSelection,
 | 
						|
  $insertNodes,
 | 
						|
  $isDecoratorNode,
 | 
						|
  $isElementNode,
 | 
						|
  $isNodeSelection,
 | 
						|
  $isRangeSelection,
 | 
						|
  $isRootNode,
 | 
						|
  $isTextNode,
 | 
						|
  $normalizeSelection__EXPERIMENTAL,
 | 
						|
  $selectAll,
 | 
						|
  $setSelection,
 | 
						|
  CLICK_COMMAND,
 | 
						|
  COMMAND_PRIORITY_EDITOR,
 | 
						|
  CONTROLLED_TEXT_INSERTION_COMMAND,
 | 
						|
  COPY_COMMAND,
 | 
						|
  createCommand,
 | 
						|
  CUT_COMMAND,
 | 
						|
  DELETE_CHARACTER_COMMAND,
 | 
						|
  DELETE_LINE_COMMAND,
 | 
						|
  DELETE_WORD_COMMAND,
 | 
						|
  DRAGOVER_COMMAND,
 | 
						|
  DRAGSTART_COMMAND,
 | 
						|
  DROP_COMMAND,
 | 
						|
  ElementNode,
 | 
						|
  FORMAT_ELEMENT_COMMAND,
 | 
						|
  FORMAT_TEXT_COMMAND,
 | 
						|
  INDENT_CONTENT_COMMAND,
 | 
						|
  INSERT_LINE_BREAK_COMMAND,
 | 
						|
  INSERT_PARAGRAPH_COMMAND,
 | 
						|
  INSERT_TAB_COMMAND,
 | 
						|
  isSelectionCapturedInDecoratorInput,
 | 
						|
  KEY_ARROW_DOWN_COMMAND,
 | 
						|
  KEY_ARROW_LEFT_COMMAND,
 | 
						|
  KEY_ARROW_RIGHT_COMMAND,
 | 
						|
  KEY_ARROW_UP_COMMAND,
 | 
						|
  KEY_BACKSPACE_COMMAND,
 | 
						|
  KEY_DELETE_COMMAND,
 | 
						|
  KEY_ENTER_COMMAND,
 | 
						|
  KEY_ESCAPE_COMMAND,
 | 
						|
  OUTDENT_CONTENT_COMMAND,
 | 
						|
  PASTE_COMMAND,
 | 
						|
  REMOVE_TEXT_COMMAND,
 | 
						|
  SELECT_ALL_COMMAND,
 | 
						|
} from 'lexical';
 | 
						|
import caretFromPoint from 'lexical/shared/caretFromPoint';
 | 
						|
import {
 | 
						|
  CAN_USE_BEFORE_INPUT,
 | 
						|
  IS_APPLE_WEBKIT,
 | 
						|
  IS_IOS,
 | 
						|
  IS_SAFARI,
 | 
						|
} from 'lexical/shared/environment';
 | 
						|
 | 
						|
export type SerializedHeadingNode = Spread<
 | 
						|
  {
 | 
						|
    tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
						|
  },
 | 
						|
  SerializedElementNode
 | 
						|
>;
 | 
						|
 | 
						|
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
 | 
						|
  'DRAG_DROP_PASTE_FILE',
 | 
						|
);
 | 
						|
 | 
						|
export type SerializedQuoteNode = SerializedElementNode;
 | 
						|
 | 
						|
/** @noInheritDoc */
 | 
						|
export class QuoteNode extends ElementNode {
 | 
						|
  static getType(): string {
 | 
						|
    return 'quote';
 | 
						|
  }
 | 
						|
 | 
						|
  static clone(node: QuoteNode): QuoteNode {
 | 
						|
    return new QuoteNode(node.__key);
 | 
						|
  }
 | 
						|
 | 
						|
  constructor(key?: NodeKey) {
 | 
						|
    super(key);
 | 
						|
  }
 | 
						|
 | 
						|
  // View
 | 
						|
 | 
						|
  createDOM(config: EditorConfig): HTMLElement {
 | 
						|
    const element = document.createElement('blockquote');
 | 
						|
    addClassNamesToElement(element, config.theme.quote);
 | 
						|
    return element;
 | 
						|
  }
 | 
						|
  updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  static importDOM(): DOMConversionMap | null {
 | 
						|
    return {
 | 
						|
      blockquote: (node: Node) => ({
 | 
						|
        conversion: $convertBlockquoteElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
						|
    const {element} = super.exportDOM(editor);
 | 
						|
 | 
						|
    if (element && isHTMLElement(element)) {
 | 
						|
      if (this.isEmpty()) {
 | 
						|
        element.append(document.createElement('br'));
 | 
						|
      }
 | 
						|
 | 
						|
      const formatType = this.getFormatType();
 | 
						|
      element.style.textAlign = formatType;
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      element,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
 | 
						|
    const node = $createQuoteNode();
 | 
						|
    node.setFormat(serializedNode.format);
 | 
						|
    node.setIndent(serializedNode.indent);
 | 
						|
    return node;
 | 
						|
  }
 | 
						|
 | 
						|
  exportJSON(): SerializedElementNode {
 | 
						|
    return {
 | 
						|
      ...super.exportJSON(),
 | 
						|
      type: 'quote',
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  // Mutation
 | 
						|
 | 
						|
  insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
 | 
						|
    const newBlock = $createParagraphNode();
 | 
						|
    const direction = this.getDirection();
 | 
						|
    newBlock.setDirection(direction);
 | 
						|
    this.insertAfter(newBlock, restoreSelection);
 | 
						|
    return newBlock;
 | 
						|
  }
 | 
						|
 | 
						|
  collapseAtStart(): true {
 | 
						|
    const paragraph = $createParagraphNode();
 | 
						|
    const children = this.getChildren();
 | 
						|
    children.forEach((child) => paragraph.append(child));
 | 
						|
    this.replace(paragraph);
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  canMergeWhenEmpty(): true {
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function $createQuoteNode(): QuoteNode {
 | 
						|
  return $applyNodeReplacement(new QuoteNode());
 | 
						|
}
 | 
						|
 | 
						|
export function $isQuoteNode(
 | 
						|
  node: LexicalNode | null | undefined,
 | 
						|
): node is QuoteNode {
 | 
						|
  return node instanceof QuoteNode;
 | 
						|
}
 | 
						|
 | 
						|
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
						|
 | 
						|
/** @noInheritDoc */
 | 
						|
export class HeadingNode extends ElementNode {
 | 
						|
  /** @internal */
 | 
						|
  __tag: HeadingTagType;
 | 
						|
 | 
						|
  static getType(): string {
 | 
						|
    return 'heading';
 | 
						|
  }
 | 
						|
 | 
						|
  static clone(node: HeadingNode): HeadingNode {
 | 
						|
    return new HeadingNode(node.__tag, node.__key);
 | 
						|
  }
 | 
						|
 | 
						|
  constructor(tag: HeadingTagType, key?: NodeKey) {
 | 
						|
    super(key);
 | 
						|
    this.__tag = tag;
 | 
						|
  }
 | 
						|
 | 
						|
  getTag(): HeadingTagType {
 | 
						|
    return this.__tag;
 | 
						|
  }
 | 
						|
 | 
						|
  // View
 | 
						|
 | 
						|
  createDOM(config: EditorConfig): HTMLElement {
 | 
						|
    const tag = this.__tag;
 | 
						|
    const element = document.createElement(tag);
 | 
						|
    const theme = config.theme;
 | 
						|
    const classNames = theme.heading;
 | 
						|
    if (classNames !== undefined) {
 | 
						|
      const className = classNames[tag];
 | 
						|
      addClassNamesToElement(element, className);
 | 
						|
    }
 | 
						|
    return element;
 | 
						|
  }
 | 
						|
 | 
						|
  updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
 | 
						|
  static importDOM(): DOMConversionMap | null {
 | 
						|
    return {
 | 
						|
      h1: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      h2: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      h3: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      h4: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      h5: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      h6: (node: Node) => ({
 | 
						|
        conversion: $convertHeadingElement,
 | 
						|
        priority: 0,
 | 
						|
      }),
 | 
						|
      p: (node: Node) => {
 | 
						|
        // domNode is a <p> since we matched it by nodeName
 | 
						|
        const paragraph = node as HTMLParagraphElement;
 | 
						|
        const firstChild = paragraph.firstChild;
 | 
						|
        if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
 | 
						|
          return {
 | 
						|
            conversion: () => ({node: null}),
 | 
						|
            priority: 3,
 | 
						|
          };
 | 
						|
        }
 | 
						|
        return null;
 | 
						|
      },
 | 
						|
      span: (node: Node) => {
 | 
						|
        if (isGoogleDocsTitle(node)) {
 | 
						|
          return {
 | 
						|
            conversion: (domNode: Node) => {
 | 
						|
              return {
 | 
						|
                node: $createHeadingNode('h1'),
 | 
						|
              };
 | 
						|
            },
 | 
						|
            priority: 3,
 | 
						|
          };
 | 
						|
        }
 | 
						|
        return null;
 | 
						|
      },
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
						|
    const {element} = super.exportDOM(editor);
 | 
						|
 | 
						|
    if (element && isHTMLElement(element)) {
 | 
						|
      if (this.isEmpty()) {
 | 
						|
        element.append(document.createElement('br'));
 | 
						|
      }
 | 
						|
 | 
						|
      const formatType = this.getFormatType();
 | 
						|
      element.style.textAlign = formatType;
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      element,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
 | 
						|
    const node = $createHeadingNode(serializedNode.tag);
 | 
						|
    node.setFormat(serializedNode.format);
 | 
						|
    node.setIndent(serializedNode.indent);
 | 
						|
    return node;
 | 
						|
  }
 | 
						|
 | 
						|
  exportJSON(): SerializedHeadingNode {
 | 
						|
    return {
 | 
						|
      ...super.exportJSON(),
 | 
						|
      tag: this.getTag(),
 | 
						|
      type: 'heading',
 | 
						|
      version: 1,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  // Mutation
 | 
						|
  insertNewAfter(
 | 
						|
    selection?: RangeSelection,
 | 
						|
    restoreSelection = true,
 | 
						|
  ): ParagraphNode | HeadingNode {
 | 
						|
    const anchorOffet = selection ? selection.anchor.offset : 0;
 | 
						|
    const lastDesc = this.getLastDescendant();
 | 
						|
    const isAtEnd =
 | 
						|
      !lastDesc ||
 | 
						|
      (selection &&
 | 
						|
        selection.anchor.key === lastDesc.getKey() &&
 | 
						|
        anchorOffet === lastDesc.getTextContentSize());
 | 
						|
    const newElement =
 | 
						|
      isAtEnd || !selection
 | 
						|
        ? $createParagraphNode()
 | 
						|
        : $createHeadingNode(this.getTag());
 | 
						|
    const direction = this.getDirection();
 | 
						|
    newElement.setDirection(direction);
 | 
						|
    this.insertAfter(newElement, restoreSelection);
 | 
						|
    if (anchorOffet === 0 && !this.isEmpty() && selection) {
 | 
						|
      const paragraph = $createParagraphNode();
 | 
						|
      paragraph.select();
 | 
						|
      this.replace(paragraph, true);
 | 
						|
    }
 | 
						|
    return newElement;
 | 
						|
  }
 | 
						|
 | 
						|
  collapseAtStart(): true {
 | 
						|
    const newElement = !this.isEmpty()
 | 
						|
      ? $createHeadingNode(this.getTag())
 | 
						|
      : $createParagraphNode();
 | 
						|
    const children = this.getChildren();
 | 
						|
    children.forEach((child) => newElement.append(child));
 | 
						|
    this.replace(newElement);
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  extractWithChild(): boolean {
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function isGoogleDocsTitle(domNode: Node): boolean {
 | 
						|
  if (domNode.nodeName.toLowerCase() === 'span') {
 | 
						|
    return (domNode as HTMLSpanElement).style.fontSize === '26pt';
 | 
						|
  }
 | 
						|
  return false;
 | 
						|
}
 | 
						|
 | 
						|
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
 | 
						|
  const nodeName = element.nodeName.toLowerCase();
 | 
						|
  let node = null;
 | 
						|
  if (
 | 
						|
    nodeName === 'h1' ||
 | 
						|
    nodeName === 'h2' ||
 | 
						|
    nodeName === 'h3' ||
 | 
						|
    nodeName === 'h4' ||
 | 
						|
    nodeName === 'h5' ||
 | 
						|
    nodeName === 'h6'
 | 
						|
  ) {
 | 
						|
    node = $createHeadingNode(nodeName);
 | 
						|
    if (element.style !== null) {
 | 
						|
      node.setFormat(element.style.textAlign as ElementFormatType);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return {node};
 | 
						|
}
 | 
						|
 | 
						|
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
 | 
						|
  const node = $createQuoteNode();
 | 
						|
  if (element.style !== null) {
 | 
						|
    node.setFormat(element.style.textAlign as ElementFormatType);
 | 
						|
  }
 | 
						|
  return {node};
 | 
						|
}
 | 
						|
 | 
						|
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
 | 
						|
  return $applyNodeReplacement(new HeadingNode(headingTag));
 | 
						|
}
 | 
						|
 | 
						|
export function $isHeadingNode(
 | 
						|
  node: LexicalNode | null | undefined,
 | 
						|
): node is HeadingNode {
 | 
						|
  return node instanceof HeadingNode;
 | 
						|
}
 | 
						|
 | 
						|
function onPasteForRichText(
 | 
						|
  event: CommandPayloadType<typeof PASTE_COMMAND>,
 | 
						|
  editor: LexicalEditor,
 | 
						|
): void {
 | 
						|
  event.preventDefault();
 | 
						|
  editor.update(
 | 
						|
    () => {
 | 
						|
      const selection = $getSelection();
 | 
						|
      const clipboardData =
 | 
						|
        objectKlassEquals(event, InputEvent) ||
 | 
						|
        objectKlassEquals(event, KeyboardEvent)
 | 
						|
          ? null
 | 
						|
          : (event as ClipboardEvent).clipboardData;
 | 
						|
      if (clipboardData != null && selection !== null) {
 | 
						|
        $insertDataTransferForRichText(clipboardData, selection, editor);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    {
 | 
						|
      tag: 'paste',
 | 
						|
    },
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
async function onCutForRichText(
 | 
						|
  event: CommandPayloadType<typeof CUT_COMMAND>,
 | 
						|
  editor: LexicalEditor,
 | 
						|
): Promise<void> {
 | 
						|
  await copyToClipboard(
 | 
						|
    editor,
 | 
						|
    objectKlassEquals(event, ClipboardEvent) ? (event as ClipboardEvent) : null,
 | 
						|
  );
 | 
						|
  editor.update(() => {
 | 
						|
    const selection = $getSelection();
 | 
						|
    if ($isRangeSelection(selection)) {
 | 
						|
      selection.removeText();
 | 
						|
    } else if ($isNodeSelection(selection)) {
 | 
						|
      selection.getNodes().forEach((node) => node.remove());
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
// Clipboard may contain files that we aren't allowed to read. While the event is arguably useless,
 | 
						|
// in certain occasions, we want to know whether it was a file transfer, as opposed to text. We
 | 
						|
// control this with the first boolean flag.
 | 
						|
export function eventFiles(
 | 
						|
  event: DragEvent | PasteCommandType,
 | 
						|
): [boolean, Array<File>, boolean] {
 | 
						|
  let dataTransfer: null | DataTransfer = null;
 | 
						|
  if (objectKlassEquals(event, DragEvent)) {
 | 
						|
    dataTransfer = (event as DragEvent).dataTransfer;
 | 
						|
  } else if (objectKlassEquals(event, ClipboardEvent)) {
 | 
						|
    dataTransfer = (event as ClipboardEvent).clipboardData;
 | 
						|
  }
 | 
						|
 | 
						|
  if (dataTransfer === null) {
 | 
						|
    return [false, [], false];
 | 
						|
  }
 | 
						|
 | 
						|
  const types = dataTransfer.types;
 | 
						|
  const hasFiles = types.includes('Files');
 | 
						|
  const hasContent =
 | 
						|
    types.includes('text/html') || types.includes('text/plain');
 | 
						|
  return [hasFiles, Array.from(dataTransfer.files), hasContent];
 | 
						|
}
 | 
						|
 | 
						|
function $handleIndentAndOutdent(
 | 
						|
  indentOrOutdent: (block: ElementNode) => void,
 | 
						|
): boolean {
 | 
						|
  const selection = $getSelection();
 | 
						|
  if (!$isRangeSelection(selection)) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  const alreadyHandled = new Set();
 | 
						|
  const nodes = selection.getNodes();
 | 
						|
  for (let i = 0; i < nodes.length; i++) {
 | 
						|
    const node = nodes[i];
 | 
						|
    const key = node.getKey();
 | 
						|
    if (alreadyHandled.has(key)) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    const parentBlock = $findMatchingParent(
 | 
						|
      node,
 | 
						|
      (parentNode): parentNode is ElementNode =>
 | 
						|
        $isElementNode(parentNode) && !parentNode.isInline(),
 | 
						|
    );
 | 
						|
    if (parentBlock === null) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    const parentKey = parentBlock.getKey();
 | 
						|
    if (parentBlock.canIndent() && !alreadyHandled.has(parentKey)) {
 | 
						|
      alreadyHandled.add(parentKey);
 | 
						|
      indentOrOutdent(parentBlock);
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return alreadyHandled.size > 0;
 | 
						|
}
 | 
						|
 | 
						|
function $isTargetWithinDecorator(target: HTMLElement): boolean {
 | 
						|
  const node = $getNearestNodeFromDOMNode(target);
 | 
						|
  return $isDecoratorNode(node);
 | 
						|
}
 | 
						|
 | 
						|
function $isSelectionAtEndOfRoot(selection: RangeSelection) {
 | 
						|
  const focus = selection.focus;
 | 
						|
  return focus.key === 'root' && focus.offset === $getRoot().getChildrenSize();
 | 
						|
}
 | 
						|
 | 
						|
export function registerRichText(editor: LexicalEditor): () => void {
 | 
						|
  const removeListener = mergeRegister(
 | 
						|
    editor.registerCommand(
 | 
						|
      CLICK_COMMAND,
 | 
						|
      (payload) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if ($isNodeSelection(selection)) {
 | 
						|
          selection.clear();
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      0,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<boolean>(
 | 
						|
      DELETE_CHARACTER_COMMAND,
 | 
						|
      (isBackward) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.deleteCharacter(isBackward);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<boolean>(
 | 
						|
      DELETE_WORD_COMMAND,
 | 
						|
      (isBackward) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.deleteWord(isBackward);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<boolean>(
 | 
						|
      DELETE_LINE_COMMAND,
 | 
						|
      (isBackward) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.deleteLine(isBackward);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      CONTROLLED_TEXT_INSERTION_COMMAND,
 | 
						|
      (eventOrText) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
 | 
						|
        if (typeof eventOrText === 'string') {
 | 
						|
          if (selection !== null) {
 | 
						|
            selection.insertText(eventOrText);
 | 
						|
          }
 | 
						|
        } else {
 | 
						|
          if (selection === null) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
 | 
						|
          const dataTransfer = eventOrText.dataTransfer;
 | 
						|
          if (dataTransfer != null) {
 | 
						|
            $insertDataTransferForRichText(dataTransfer, selection, editor);
 | 
						|
          } else if ($isRangeSelection(selection)) {
 | 
						|
            const data = eventOrText.data;
 | 
						|
            if (data) {
 | 
						|
              selection.insertText(data);
 | 
						|
            }
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      REMOVE_TEXT_COMMAND,
 | 
						|
      () => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.removeText();
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<TextFormatType>(
 | 
						|
      FORMAT_TEXT_COMMAND,
 | 
						|
      (format) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.formatText(format);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<ElementFormatType>(
 | 
						|
      FORMAT_ELEMENT_COMMAND,
 | 
						|
      (format) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection) && !$isNodeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        const nodes = selection.getNodes();
 | 
						|
        for (const node of nodes) {
 | 
						|
          const element = $findMatchingParent(
 | 
						|
            node,
 | 
						|
            (parentNode): parentNode is ElementNode =>
 | 
						|
              $isElementNode(parentNode) && !parentNode.isInline(),
 | 
						|
          );
 | 
						|
          if (element !== null) {
 | 
						|
            element.setFormat(format);
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<boolean>(
 | 
						|
      INSERT_LINE_BREAK_COMMAND,
 | 
						|
      (selectStart) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.insertLineBreak(selectStart);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      INSERT_PARAGRAPH_COMMAND,
 | 
						|
      () => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        selection.insertParagraph();
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      INSERT_TAB_COMMAND,
 | 
						|
      () => {
 | 
						|
        $insertNodes([$createTabNode()]);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      INDENT_CONTENT_COMMAND,
 | 
						|
      () => {
 | 
						|
        return $handleIndentAndOutdent((block) => {
 | 
						|
          const indent = block.getIndent();
 | 
						|
          block.setIndent(indent + 1);
 | 
						|
        });
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      OUTDENT_CONTENT_COMMAND,
 | 
						|
      () => {
 | 
						|
        return $handleIndentAndOutdent((block) => {
 | 
						|
          const indent = block.getIndent();
 | 
						|
          if (indent > 0) {
 | 
						|
            block.setIndent(indent - 1);
 | 
						|
          }
 | 
						|
        });
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_ARROW_UP_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (
 | 
						|
          $isNodeSelection(selection) &&
 | 
						|
          !$isTargetWithinDecorator(event.target as HTMLElement)
 | 
						|
        ) {
 | 
						|
          // If selection is on a node, let's try and move selection
 | 
						|
          // back to being a range selection.
 | 
						|
          const nodes = selection.getNodes();
 | 
						|
          if (nodes.length > 0) {
 | 
						|
            nodes[0].selectPrevious();
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        } else if ($isRangeSelection(selection)) {
 | 
						|
          const possibleNode = $getAdjacentNode(selection.focus, true);
 | 
						|
          if (
 | 
						|
            !event.shiftKey &&
 | 
						|
            $isDecoratorNode(possibleNode) &&
 | 
						|
            !possibleNode.isIsolated() &&
 | 
						|
            !possibleNode.isInline()
 | 
						|
          ) {
 | 
						|
            possibleNode.selectPrevious();
 | 
						|
            event.preventDefault();
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_ARROW_DOWN_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if ($isNodeSelection(selection)) {
 | 
						|
          // If selection is on a node, let's try and move selection
 | 
						|
          // back to being a range selection.
 | 
						|
          const nodes = selection.getNodes();
 | 
						|
          if (nodes.length > 0) {
 | 
						|
            nodes[0].selectNext(0, 0);
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        } else if ($isRangeSelection(selection)) {
 | 
						|
          if ($isSelectionAtEndOfRoot(selection)) {
 | 
						|
            event.preventDefault();
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
          const possibleNode = $getAdjacentNode(selection.focus, false);
 | 
						|
          if (
 | 
						|
            !event.shiftKey &&
 | 
						|
            $isDecoratorNode(possibleNode) &&
 | 
						|
            !possibleNode.isIsolated() &&
 | 
						|
            !possibleNode.isInline()
 | 
						|
          ) {
 | 
						|
            possibleNode.selectNext();
 | 
						|
            event.preventDefault();
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_ARROW_LEFT_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if ($isNodeSelection(selection)) {
 | 
						|
          // If selection is on a node, let's try and move selection
 | 
						|
          // back to being a range selection.
 | 
						|
          const nodes = selection.getNodes();
 | 
						|
          if (nodes.length > 0) {
 | 
						|
            event.preventDefault();
 | 
						|
            nodes[0].selectPrevious();
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if ($shouldOverrideDefaultCharacterSelection(selection, true)) {
 | 
						|
          const isHoldingShift = event.shiftKey;
 | 
						|
          event.preventDefault();
 | 
						|
          $moveCharacter(selection, isHoldingShift, true);
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_ARROW_RIGHT_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (
 | 
						|
          $isNodeSelection(selection) &&
 | 
						|
          !$isTargetWithinDecorator(event.target as HTMLElement)
 | 
						|
        ) {
 | 
						|
          // If selection is on a node, let's try and move selection
 | 
						|
          // back to being a range selection.
 | 
						|
          const nodes = selection.getNodes();
 | 
						|
          if (nodes.length > 0) {
 | 
						|
            event.preventDefault();
 | 
						|
            nodes[0].selectNext(0, 0);
 | 
						|
            return true;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        const isHoldingShift = event.shiftKey;
 | 
						|
        if ($shouldOverrideDefaultCharacterSelection(selection, false)) {
 | 
						|
          event.preventDefault();
 | 
						|
          $moveCharacter(selection, isHoldingShift, false);
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_BACKSPACE_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        if ($isTargetWithinDecorator(event.target as HTMLElement)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        event.preventDefault();
 | 
						|
        const {anchor} = selection;
 | 
						|
        const anchorNode = anchor.getNode();
 | 
						|
 | 
						|
        if (
 | 
						|
          selection.isCollapsed() &&
 | 
						|
          anchor.offset === 0 &&
 | 
						|
          !$isRootNode(anchorNode)
 | 
						|
        ) {
 | 
						|
          const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
 | 
						|
          if (element.getIndent() > 0) {
 | 
						|
            return editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined);
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, true);
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent>(
 | 
						|
      KEY_DELETE_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        if ($isTargetWithinDecorator(event.target as HTMLElement)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        event.preventDefault();
 | 
						|
        return editor.dispatchCommand(DELETE_CHARACTER_COMMAND, false);
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<KeyboardEvent | null>(
 | 
						|
      KEY_ENTER_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (event !== null) {
 | 
						|
          // If we have beforeinput, then we can avoid blocking
 | 
						|
          // the default behavior. This ensures that the iOS can
 | 
						|
          // intercept that we're actually inserting a paragraph,
 | 
						|
          // and autocomplete, autocapitalize etc work as intended.
 | 
						|
          // This can also cause a strange performance issue in
 | 
						|
          // Safari, where there is a noticeable pause due to
 | 
						|
          // preventing the key down of enter.
 | 
						|
          if (
 | 
						|
            (IS_IOS || IS_SAFARI || IS_APPLE_WEBKIT) &&
 | 
						|
            CAN_USE_BEFORE_INPUT
 | 
						|
          ) {
 | 
						|
            return false;
 | 
						|
          }
 | 
						|
          event.preventDefault();
 | 
						|
          if (event.shiftKey) {
 | 
						|
            return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return editor.dispatchCommand(INSERT_PARAGRAPH_COMMAND, undefined);
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      KEY_ESCAPE_COMMAND,
 | 
						|
      () => {
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (!$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        editor.blur();
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<DragEvent>(
 | 
						|
      DROP_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const [, files] = eventFiles(event);
 | 
						|
        if (files.length > 0) {
 | 
						|
          const x = event.clientX;
 | 
						|
          const y = event.clientY;
 | 
						|
          const eventRange = caretFromPoint(x, y);
 | 
						|
          if (eventRange !== null) {
 | 
						|
            const {offset: domOffset, node: domNode} = eventRange;
 | 
						|
            const node = $getNearestNodeFromDOMNode(domNode);
 | 
						|
            if (node !== null) {
 | 
						|
              const selection = $createRangeSelection();
 | 
						|
              if ($isTextNode(node)) {
 | 
						|
                selection.anchor.set(node.getKey(), domOffset, 'text');
 | 
						|
                selection.focus.set(node.getKey(), domOffset, 'text');
 | 
						|
              } else {
 | 
						|
                const parentKey = node.getParentOrThrow().getKey();
 | 
						|
                const offset = node.getIndexWithinParent() + 1;
 | 
						|
                selection.anchor.set(parentKey, offset, 'element');
 | 
						|
                selection.focus.set(parentKey, offset, 'element');
 | 
						|
              }
 | 
						|
              const normalizedSelection =
 | 
						|
                $normalizeSelection__EXPERIMENTAL(selection);
 | 
						|
              $setSelection(normalizedSelection);
 | 
						|
            }
 | 
						|
            editor.dispatchCommand(DRAG_DROP_PASTE, files);
 | 
						|
          }
 | 
						|
          event.preventDefault();
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
 | 
						|
        const selection = $getSelection();
 | 
						|
        if ($isRangeSelection(selection)) {
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<DragEvent>(
 | 
						|
      DRAGSTART_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const [isFileTransfer] = eventFiles(event);
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (isFileTransfer && !$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand<DragEvent>(
 | 
						|
      DRAGOVER_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const [isFileTransfer] = eventFiles(event);
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (isFileTransfer && !$isRangeSelection(selection)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        const x = event.clientX;
 | 
						|
        const y = event.clientY;
 | 
						|
        const eventRange = caretFromPoint(x, y);
 | 
						|
        if (eventRange !== null) {
 | 
						|
          const node = $getNearestNodeFromDOMNode(eventRange.node);
 | 
						|
          if ($isDecoratorNode(node)) {
 | 
						|
            // Show browser caret as the user is dragging the media across the screen. Won't work
 | 
						|
            // for DecoratorNode nor it's relevant.
 | 
						|
            event.preventDefault();
 | 
						|
          }
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      SELECT_ALL_COMMAND,
 | 
						|
      () => {
 | 
						|
        $selectAll();
 | 
						|
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      COPY_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        copyToClipboard(
 | 
						|
          editor,
 | 
						|
          objectKlassEquals(event, ClipboardEvent)
 | 
						|
            ? (event as ClipboardEvent)
 | 
						|
            : null,
 | 
						|
        );
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      CUT_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        onCutForRichText(event, editor);
 | 
						|
        return true;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
    editor.registerCommand(
 | 
						|
      PASTE_COMMAND,
 | 
						|
      (event) => {
 | 
						|
        const [, files, hasTextContent] = eventFiles(event);
 | 
						|
        if (files.length > 0 && !hasTextContent) {
 | 
						|
          editor.dispatchCommand(DRAG_DROP_PASTE, files);
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
 | 
						|
        // if inputs then paste within the input ignore creating a new node on paste event
 | 
						|
        if (isSelectionCapturedInDecoratorInput(event.target as Node)) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
 | 
						|
        const selection = $getSelection();
 | 
						|
        if (selection !== null) {
 | 
						|
          onPasteForRichText(event, editor);
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
      },
 | 
						|
      COMMAND_PRIORITY_EDITOR,
 | 
						|
    ),
 | 
						|
  );
 | 
						|
  return removeListener;
 | 
						|
}
 |