1383 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			1383 lines
		
	
	
		
			44 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 {LexicalEditor} from './LexicalEditor';
							 | 
						|||
| 
								 | 
							
								import type {NodeKey} from './LexicalNode';
							 | 
						|||
| 
								 | 
							
								import type {ElementNode} from './nodes/LexicalElementNode';
							 | 
						|||
| 
								 | 
							
								import type {TextNode} from './nodes/LexicalTextNode';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								import {
							 | 
						|||
| 
								 | 
							
								  CAN_USE_BEFORE_INPUT,
							 | 
						|||
| 
								 | 
							
								  IS_ANDROID_CHROME,
							 | 
						|||
| 
								 | 
							
								  IS_APPLE_WEBKIT,
							 | 
						|||
| 
								 | 
							
								  IS_FIREFOX,
							 | 
						|||
| 
								 | 
							
								  IS_IOS,
							 | 
						|||
| 
								 | 
							
								  IS_SAFARI,
							 | 
						|||
| 
								 | 
							
								} from 'lexical/shared/environment';
							 | 
						|||
| 
								 | 
							
								import invariant from 'lexical/shared/invariant';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								import {
							 | 
						|||
| 
								 | 
							
								  $getPreviousSelection,
							 | 
						|||
| 
								 | 
							
								  $getRoot,
							 | 
						|||
| 
								 | 
							
								  $getSelection,
							 | 
						|||
| 
								 | 
							
								  $isElementNode,
							 | 
						|||
| 
								 | 
							
								  $isNodeSelection,
							 | 
						|||
| 
								 | 
							
								  $isRangeSelection,
							 | 
						|||
| 
								 | 
							
								  $isRootNode,
							 | 
						|||
| 
								 | 
							
								  $isTextNode,
							 | 
						|||
| 
								 | 
							
								  $setCompositionKey,
							 | 
						|||
| 
								 | 
							
								  BLUR_COMMAND,
							 | 
						|||
| 
								 | 
							
								  CLICK_COMMAND,
							 | 
						|||
| 
								 | 
							
								  CONTROLLED_TEXT_INSERTION_COMMAND,
							 | 
						|||
| 
								 | 
							
								  COPY_COMMAND,
							 | 
						|||
| 
								 | 
							
								  CUT_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DELETE_CHARACTER_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DELETE_LINE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DELETE_WORD_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DRAGEND_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DRAGOVER_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DRAGSTART_COMMAND,
							 | 
						|||
| 
								 | 
							
								  DROP_COMMAND,
							 | 
						|||
| 
								 | 
							
								  FOCUS_COMMAND,
							 | 
						|||
| 
								 | 
							
								  FORMAT_TEXT_COMMAND,
							 | 
						|||
| 
								 | 
							
								  INSERT_LINE_BREAK_COMMAND,
							 | 
						|||
| 
								 | 
							
								  INSERT_PARAGRAPH_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ARROW_DOWN_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ARROW_LEFT_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ARROW_RIGHT_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ARROW_UP_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_BACKSPACE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_DELETE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_DOWN_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ENTER_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_ESCAPE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_SPACE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  KEY_TAB_COMMAND,
							 | 
						|||
| 
								 | 
							
								  MOVE_TO_END,
							 | 
						|||
| 
								 | 
							
								  MOVE_TO_START,
							 | 
						|||
| 
								 | 
							
								  ParagraphNode,
							 | 
						|||
| 
								 | 
							
								  PASTE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  REDO_COMMAND,
							 | 
						|||
| 
								 | 
							
								  REMOVE_TEXT_COMMAND,
							 | 
						|||
| 
								 | 
							
								  SELECTION_CHANGE_COMMAND,
							 | 
						|||
| 
								 | 
							
								  UNDO_COMMAND,
							 | 
						|||
| 
								 | 
							
								} from '.';
							 | 
						|||
| 
								 | 
							
								import {KEY_MODIFIER_COMMAND, SELECT_ALL_COMMAND} from './LexicalCommands';
							 | 
						|||
| 
								 | 
							
								import {
							 | 
						|||
| 
								 | 
							
								  COMPOSITION_START_CHAR,
							 | 
						|||
| 
								 | 
							
								  DOM_ELEMENT_TYPE,
							 | 
						|||
| 
								 | 
							
								  DOM_TEXT_TYPE,
							 | 
						|||
| 
								 | 
							
								  DOUBLE_LINE_BREAK,
							 | 
						|||
| 
								 | 
							
								  IS_ALL_FORMATTING,
							 | 
						|||
| 
								 | 
							
								} from './LexicalConstants';
							 | 
						|||
| 
								 | 
							
								import {
							 | 
						|||
| 
								 | 
							
								  $internalCreateRangeSelection,
							 | 
						|||
| 
								 | 
							
								  RangeSelection,
							 | 
						|||
| 
								 | 
							
								} from './LexicalSelection';
							 | 
						|||
| 
								 | 
							
								import {getActiveEditor, updateEditor} from './LexicalUpdates';
							 | 
						|||
| 
								 | 
							
								import {
							 | 
						|||
| 
								 | 
							
								  $flushMutations,
							 | 
						|||
| 
								 | 
							
								  $getNodeByKey,
							 | 
						|||
| 
								 | 
							
								  $isSelectionCapturedInDecorator,
							 | 
						|||
| 
								 | 
							
								  $isTokenOrSegmented,
							 | 
						|||
| 
								 | 
							
								  $setSelection,
							 | 
						|||
| 
								 | 
							
								  $shouldInsertTextAfterOrBeforeTextNode,
							 | 
						|||
| 
								 | 
							
								  $updateSelectedTextFromDOM,
							 | 
						|||
| 
								 | 
							
								  $updateTextNodeFromDOMContent,
							 | 
						|||
| 
								 | 
							
								  dispatchCommand,
							 | 
						|||
| 
								 | 
							
								  doesContainGrapheme,
							 | 
						|||
| 
								 | 
							
								  getAnchorTextFromDOM,
							 | 
						|||
| 
								 | 
							
								  getDOMSelection,
							 | 
						|||
| 
								 | 
							
								  getDOMTextNode,
							 | 
						|||
| 
								 | 
							
								  getEditorPropertyFromDOMNode,
							 | 
						|||
| 
								 | 
							
								  getEditorsToPropagate,
							 | 
						|||
| 
								 | 
							
								  getNearestEditorFromDOMNode,
							 | 
						|||
| 
								 | 
							
								  getWindow,
							 | 
						|||
| 
								 | 
							
								  isBackspace,
							 | 
						|||
| 
								 | 
							
								  isBold,
							 | 
						|||
| 
								 | 
							
								  isCopy,
							 | 
						|||
| 
								 | 
							
								  isCut,
							 | 
						|||
| 
								 | 
							
								  isDelete,
							 | 
						|||
| 
								 | 
							
								  isDeleteBackward,
							 | 
						|||
| 
								 | 
							
								  isDeleteForward,
							 | 
						|||
| 
								 | 
							
								  isDeleteLineBackward,
							 | 
						|||
| 
								 | 
							
								  isDeleteLineForward,
							 | 
						|||
| 
								 | 
							
								  isDeleteWordBackward,
							 | 
						|||
| 
								 | 
							
								  isDeleteWordForward,
							 | 
						|||
| 
								 | 
							
								  isEscape,
							 | 
						|||
| 
								 | 
							
								  isFirefoxClipboardEvents,
							 | 
						|||
| 
								 | 
							
								  isItalic,
							 | 
						|||
| 
								 | 
							
								  isLexicalEditor,
							 | 
						|||
| 
								 | 
							
								  isLineBreak,
							 | 
						|||
| 
								 | 
							
								  isModifier,
							 | 
						|||
| 
								 | 
							
								  isMoveBackward,
							 | 
						|||
| 
								 | 
							
								  isMoveDown,
							 | 
						|||
| 
								 | 
							
								  isMoveForward,
							 | 
						|||
| 
								 | 
							
								  isMoveToEnd,
							 | 
						|||
| 
								 | 
							
								  isMoveToStart,
							 | 
						|||
| 
								 | 
							
								  isMoveUp,
							 | 
						|||
| 
								 | 
							
								  isOpenLineBreak,
							 | 
						|||
| 
								 | 
							
								  isParagraph,
							 | 
						|||
| 
								 | 
							
								  isRedo,
							 | 
						|||
| 
								 | 
							
								  isSelectAll,
							 | 
						|||
| 
								 | 
							
								  isSelectionWithinEditor,
							 | 
						|||
| 
								 | 
							
								  isSpace,
							 | 
						|||
| 
								 | 
							
								  isTab,
							 | 
						|||
| 
								 | 
							
								  isUnderline,
							 | 
						|||
| 
								 | 
							
								  isUndo,
							 | 
						|||
| 
								 | 
							
								} from './LexicalUtils';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								type RootElementRemoveHandles = Array<() => void>;
							 | 
						|||
| 
								 | 
							
								type RootElementEvents = Array<
							 | 
						|||
| 
								 | 
							
								  [
							 | 
						|||
| 
								 | 
							
								    string,
							 | 
						|||
| 
								 | 
							
								    Record<string, unknown> | ((event: Event, editor: LexicalEditor) => void),
							 | 
						|||
| 
								 | 
							
								  ]
							 | 
						|||
| 
								 | 
							
								>;
							 | 
						|||
| 
								 | 
							
								const PASS_THROUGH_COMMAND = Object.freeze({});
							 | 
						|||
| 
								 | 
							
								const ANDROID_COMPOSITION_LATENCY = 30;
							 | 
						|||
| 
								 | 
							
								const rootElementEvents: RootElementEvents = [
							 | 
						|||
| 
								 | 
							
								  ['keydown', onKeyDown],
							 | 
						|||
| 
								 | 
							
								  ['pointerdown', onPointerDown],
							 | 
						|||
| 
								 | 
							
								  ['compositionstart', onCompositionStart],
							 | 
						|||
| 
								 | 
							
								  ['compositionend', onCompositionEnd],
							 | 
						|||
| 
								 | 
							
								  ['input', onInput],
							 | 
						|||
| 
								 | 
							
								  ['click', onClick],
							 | 
						|||
| 
								 | 
							
								  ['cut', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['copy', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['dragstart', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['dragover', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['dragend', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['paste', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['focus', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['blur', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								  ['drop', PASS_THROUGH_COMMAND],
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								if (CAN_USE_BEFORE_INPUT) {
							 | 
						|||
| 
								 | 
							
								  rootElementEvents.push([
							 | 
						|||
| 
								 | 
							
								    'beforeinput',
							 | 
						|||
| 
								 | 
							
								    (event, editor) => onBeforeInput(event as InputEvent, editor),
							 | 
						|||
| 
								 | 
							
								  ]);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								let lastKeyDownTimeStamp = 0;
							 | 
						|||
| 
								 | 
							
								let lastKeyCode: null | string = null;
							 | 
						|||
| 
								 | 
							
								let lastBeforeInputInsertTextTimeStamp = 0;
							 | 
						|||
| 
								 | 
							
								let unprocessedBeforeInputData: null | string = null;
							 | 
						|||
| 
								 | 
							
								const rootElementsRegistered = new WeakMap<Document, number>();
							 | 
						|||
| 
								 | 
							
								let isSelectionChangeFromDOMUpdate = false;
							 | 
						|||
| 
								 | 
							
								let isSelectionChangeFromMouseDown = false;
							 | 
						|||
| 
								 | 
							
								let isInsertLineBreak = false;
							 | 
						|||
| 
								 | 
							
								let isFirefoxEndingComposition = false;
							 | 
						|||
| 
								 | 
							
								let collapsedSelectionFormat: [number, string, number, NodeKey, number] = [
							 | 
						|||
| 
								 | 
							
								  0,
							 | 
						|||
| 
								 | 
							
								  '',
							 | 
						|||
| 
								 | 
							
								  0,
							 | 
						|||
| 
								 | 
							
								  'root',
							 | 
						|||
| 
								 | 
							
								  0,
							 | 
						|||
| 
								 | 
							
								];
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// This function is used to determine if Lexical should attempt to override
							 | 
						|||
| 
								 | 
							
								// the default browser behavior for insertion of text and use its own internal
							 | 
						|||
| 
								 | 
							
								// heuristics. This is an extremely important function, and makes much of Lexical
							 | 
						|||
| 
								 | 
							
								// work as intended between different browsers and across word, line and character
							 | 
						|||
| 
								 | 
							
								// boundary/formats. It also is important for text replacement, node schemas and
							 | 
						|||
| 
								 | 
							
								// composition mechanics.
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function $shouldPreventDefaultAndInsertText(
							 | 
						|||
| 
								 | 
							
								  selection: RangeSelection,
							 | 
						|||
| 
								 | 
							
								  domTargetRange: null | StaticRange,
							 | 
						|||
| 
								 | 
							
								  text: string,
							 | 
						|||
| 
								 | 
							
								  timeStamp: number,
							 | 
						|||
| 
								 | 
							
								  isBeforeInput: boolean,
							 | 
						|||
| 
								 | 
							
								): boolean {
							 | 
						|||
| 
								 | 
							
								  const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								  const focus = selection.focus;
							 | 
						|||
| 
								 | 
							
								  const anchorNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								  const editor = getActiveEditor();
							 | 
						|||
| 
								 | 
							
								  const domSelection = getDOMSelection(editor._window);
							 | 
						|||
| 
								 | 
							
								  const domAnchorNode = domSelection !== null ? domSelection.anchorNode : null;
							 | 
						|||
| 
								 | 
							
								  const anchorKey = anchor.key;
							 | 
						|||
| 
								 | 
							
								  const backingAnchorElement = editor.getElementByKey(anchorKey);
							 | 
						|||
| 
								 | 
							
								  const textLength = text.length;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  return (
							 | 
						|||
| 
								 | 
							
								    anchorKey !== focus.key ||
							 | 
						|||
| 
								 | 
							
								    // If we're working with a non-text node.
							 | 
						|||
| 
								 | 
							
								    !$isTextNode(anchorNode) ||
							 | 
						|||
| 
								 | 
							
								    // If we are replacing a range with a single character or grapheme, and not composing.
							 | 
						|||
| 
								 | 
							
								    (((!isBeforeInput &&
							 | 
						|||
| 
								 | 
							
								      (!CAN_USE_BEFORE_INPUT ||
							 | 
						|||
| 
								 | 
							
								        // We check to see if there has been
							 | 
						|||
| 
								 | 
							
								        // a recent beforeinput event for "textInput". If there has been one in the last
							 | 
						|||
| 
								 | 
							
								        // 50ms then we proceed as normal. However, if there is not, then this is likely
							 | 
						|||
| 
								 | 
							
								        // a dangling `input` event caused by execCommand('insertText').
							 | 
						|||
| 
								 | 
							
								        lastBeforeInputInsertTextTimeStamp < timeStamp + 50)) ||
							 | 
						|||
| 
								 | 
							
								      (anchorNode.isDirty() && textLength < 2) ||
							 | 
						|||
| 
								 | 
							
								      doesContainGrapheme(text)) &&
							 | 
						|||
| 
								 | 
							
								      anchor.offset !== focus.offset &&
							 | 
						|||
| 
								 | 
							
								      !anchorNode.isComposing()) ||
							 | 
						|||
| 
								 | 
							
								    // Any non standard text node.
							 | 
						|||
| 
								 | 
							
								    $isTokenOrSegmented(anchorNode) ||
							 | 
						|||
| 
								 | 
							
								    // If the text length is more than a single character and we're either
							 | 
						|||
| 
								 | 
							
								    // dealing with this in "beforeinput" or where the node has already recently
							 | 
						|||
| 
								 | 
							
								    // been changed (thus is dirty).
							 | 
						|||
| 
								 | 
							
								    (anchorNode.isDirty() && textLength > 1) ||
							 | 
						|||
| 
								 | 
							
								    // If the DOM selection element is not the same as the backing node during beforeinput.
							 | 
						|||
| 
								 | 
							
								    ((isBeforeInput || !CAN_USE_BEFORE_INPUT) &&
							 | 
						|||
| 
								 | 
							
								      backingAnchorElement !== null &&
							 | 
						|||
| 
								 | 
							
								      !anchorNode.isComposing() &&
							 | 
						|||
| 
								 | 
							
								      domAnchorNode !== getDOMTextNode(backingAnchorElement)) ||
							 | 
						|||
| 
								 | 
							
								    // If TargetRange is not the same as the DOM selection; browser trying to edit random parts
							 | 
						|||
| 
								 | 
							
								    // of the editor.
							 | 
						|||
| 
								 | 
							
								    (domSelection !== null &&
							 | 
						|||
| 
								 | 
							
								      domTargetRange !== null &&
							 | 
						|||
| 
								 | 
							
								      (!domTargetRange.collapsed ||
							 | 
						|||
| 
								 | 
							
								        domTargetRange.startContainer !== domSelection.anchorNode ||
							 | 
						|||
| 
								 | 
							
								        domTargetRange.startOffset !== domSelection.anchorOffset)) ||
							 | 
						|||
| 
								 | 
							
								    // Check if we're changing from bold to italics, or some other format.
							 | 
						|||
| 
								 | 
							
								    anchorNode.getFormat() !== selection.format ||
							 | 
						|||
| 
								 | 
							
								    anchorNode.getStyle() !== selection.style ||
							 | 
						|||
| 
								 | 
							
								    // One last set of heuristics to check against.
							 | 
						|||
| 
								 | 
							
								    $shouldInsertTextAfterOrBeforeTextNode(selection, anchorNode)
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function shouldSkipSelectionChange(
							 | 
						|||
| 
								 | 
							
								  domNode: null | Node,
							 | 
						|||
| 
								 | 
							
								  offset: number,
							 | 
						|||
| 
								 | 
							
								): boolean {
							 | 
						|||
| 
								 | 
							
								  return (
							 | 
						|||
| 
								 | 
							
								    domNode !== null &&
							 | 
						|||
| 
								 | 
							
								    domNode.nodeValue !== null &&
							 | 
						|||
| 
								 | 
							
								    domNode.nodeType === DOM_TEXT_TYPE &&
							 | 
						|||
| 
								 | 
							
								    offset !== 0 &&
							 | 
						|||
| 
								 | 
							
								    offset !== domNode.nodeValue.length
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onSelectionChange(
							 | 
						|||
| 
								 | 
							
								  domSelection: Selection,
							 | 
						|||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						|||
| 
								 | 
							
								  isActive: boolean,
							 | 
						|||
| 
								 | 
							
								): void {
							 | 
						|||
| 
								 | 
							
								  const {
							 | 
						|||
| 
								 | 
							
								    anchorNode: anchorDOM,
							 | 
						|||
| 
								 | 
							
								    anchorOffset,
							 | 
						|||
| 
								 | 
							
								    focusNode: focusDOM,
							 | 
						|||
| 
								 | 
							
								    focusOffset,
							 | 
						|||
| 
								 | 
							
								  } = domSelection;
							 | 
						|||
| 
								 | 
							
								  if (isSelectionChangeFromDOMUpdate) {
							 | 
						|||
| 
								 | 
							
								    isSelectionChangeFromDOMUpdate = false;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // If native DOM selection is on a DOM element, then
							 | 
						|||
| 
								 | 
							
								    // we should continue as usual, as Lexical's selection
							 | 
						|||
| 
								 | 
							
								    // may have normalized to a better child. If the DOM
							 | 
						|||
| 
								 | 
							
								    // element is a text node, we can safely apply this
							 | 
						|||
| 
								 | 
							
								    // optimization and skip the selection change entirely.
							 | 
						|||
| 
								 | 
							
								    // We also need to check if the offset is at the boundary,
							 | 
						|||
| 
								 | 
							
								    // because in this case, we might need to normalize to a
							 | 
						|||
| 
								 | 
							
								    // sibling instead.
							 | 
						|||
| 
								 | 
							
								    if (
							 | 
						|||
| 
								 | 
							
								      shouldSkipSelectionChange(anchorDOM, anchorOffset) &&
							 | 
						|||
| 
								 | 
							
								      shouldSkipSelectionChange(focusDOM, focusOffset)
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								    // Non-active editor don't need any extra logic for selection, it only needs update
							 | 
						|||
| 
								 | 
							
								    // to reconcile selection (set it to null) to ensure that only one editor has non-null selection.
							 | 
						|||
| 
								 | 
							
								    if (!isActive) {
							 | 
						|||
| 
								 | 
							
								      $setSelection(null);
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (!isSelectionWithinEditor(editor, anchorDOM, focusDOM)) {
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // Update the selection format
							 | 
						|||
| 
								 | 
							
								    if ($isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								      const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								      const anchorNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if (selection.isCollapsed()) {
							 | 
						|||
| 
								 | 
							
								        // Badly interpreted range selection when collapsed - #1482
							 | 
						|||
| 
								 | 
							
								        if (
							 | 
						|||
| 
								 | 
							
								          domSelection.type === 'Range' &&
							 | 
						|||
| 
								 | 
							
								          domSelection.anchorNode === domSelection.focusNode
							 | 
						|||
| 
								 | 
							
								        ) {
							 | 
						|||
| 
								 | 
							
								          selection.dirty = true;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        // If we have marked a collapsed selection format, and we're
							 | 
						|||
| 
								 | 
							
								        // within the given time range – then attempt to use that format
							 | 
						|||
| 
								 | 
							
								        // instead of getting the format from the anchor node.
							 | 
						|||
| 
								 | 
							
								        const windowEvent = getWindow(editor).event;
							 | 
						|||
| 
								 | 
							
								        const currentTimeStamp = windowEvent
							 | 
						|||
| 
								 | 
							
								          ? windowEvent.timeStamp
							 | 
						|||
| 
								 | 
							
								          : performance.now();
							 | 
						|||
| 
								 | 
							
								        const [lastFormat, lastStyle, lastOffset, lastKey, timeStamp] =
							 | 
						|||
| 
								 | 
							
								          collapsedSelectionFormat;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        const root = $getRoot();
							 | 
						|||
| 
								 | 
							
								        const isRootTextContentEmpty =
							 | 
						|||
| 
								 | 
							
								          editor.isComposing() === false && root.getTextContent() === '';
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        if (
							 | 
						|||
| 
								 | 
							
								          currentTimeStamp < timeStamp + 200 &&
							 | 
						|||
| 
								 | 
							
								          anchor.offset === lastOffset &&
							 | 
						|||
| 
								 | 
							
								          anchor.key === lastKey
							 | 
						|||
| 
								 | 
							
								        ) {
							 | 
						|||
| 
								 | 
							
								          selection.format = lastFormat;
							 | 
						|||
| 
								 | 
							
								          selection.style = lastStyle;
							 | 
						|||
| 
								 | 
							
								        } else {
							 | 
						|||
| 
								 | 
							
								          if (anchor.type === 'text') {
							 | 
						|||
| 
								 | 
							
								            invariant(
							 | 
						|||
| 
								 | 
							
								              $isTextNode(anchorNode),
							 | 
						|||
| 
								 | 
							
								              'Point.getNode() must return TextNode when type is text',
							 | 
						|||
| 
								 | 
							
								            );
							 | 
						|||
| 
								 | 
							
								            selection.format = anchorNode.getFormat();
							 | 
						|||
| 
								 | 
							
								            selection.style = anchorNode.getStyle();
							 | 
						|||
| 
								 | 
							
								          } else if (anchor.type === 'element' && !isRootTextContentEmpty) {
							 | 
						|||
| 
								 | 
							
								            const lastNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								            selection.style = '';
							 | 
						|||
| 
								 | 
							
								            if (
							 | 
						|||
| 
								 | 
							
								              lastNode instanceof ParagraphNode &&
							 | 
						|||
| 
								 | 
							
								              lastNode.getChildrenSize() === 0
							 | 
						|||
| 
								 | 
							
								            ) {
							 | 
						|||
| 
								 | 
							
								              selection.style = lastNode.getTextStyle();
							 | 
						|||
| 
								 | 
							
								            } else {
							 | 
						|||
| 
								 | 
							
								              selection.format = 0;
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else {
							 | 
						|||
| 
								 | 
							
								        const anchorKey = anchor.key;
							 | 
						|||
| 
								 | 
							
								        const focus = selection.focus;
							 | 
						|||
| 
								 | 
							
								        const focusKey = focus.key;
							 | 
						|||
| 
								 | 
							
								        const nodes = selection.getNodes();
							 | 
						|||
| 
								 | 
							
								        const nodesLength = nodes.length;
							 | 
						|||
| 
								 | 
							
								        const isBackward = selection.isBackward();
							 | 
						|||
| 
								 | 
							
								        const startOffset = isBackward ? focusOffset : anchorOffset;
							 | 
						|||
| 
								 | 
							
								        const endOffset = isBackward ? anchorOffset : focusOffset;
							 | 
						|||
| 
								 | 
							
								        const startKey = isBackward ? focusKey : anchorKey;
							 | 
						|||
| 
								 | 
							
								        const endKey = isBackward ? anchorKey : focusKey;
							 | 
						|||
| 
								 | 
							
								        let combinedFormat = IS_ALL_FORMATTING;
							 | 
						|||
| 
								 | 
							
								        let hasTextNodes = false;
							 | 
						|||
| 
								 | 
							
								        for (let i = 0; i < nodesLength; i++) {
							 | 
						|||
| 
								 | 
							
								          const node = nodes[i];
							 | 
						|||
| 
								 | 
							
								          const textContentSize = node.getTextContentSize();
							 | 
						|||
| 
								 | 
							
								          if (
							 | 
						|||
| 
								 | 
							
								            $isTextNode(node) &&
							 | 
						|||
| 
								 | 
							
								            textContentSize !== 0 &&
							 | 
						|||
| 
								 | 
							
								            // Exclude empty text nodes at boundaries resulting from user's selection
							 | 
						|||
| 
								 | 
							
								            !(
							 | 
						|||
| 
								 | 
							
								              (i === 0 &&
							 | 
						|||
| 
								 | 
							
								                node.__key === startKey &&
							 | 
						|||
| 
								 | 
							
								                startOffset === textContentSize) ||
							 | 
						|||
| 
								 | 
							
								              (i === nodesLength - 1 &&
							 | 
						|||
| 
								 | 
							
								                node.__key === endKey &&
							 | 
						|||
| 
								 | 
							
								                endOffset === 0)
							 | 
						|||
| 
								 | 
							
								            )
							 | 
						|||
| 
								 | 
							
								          ) {
							 | 
						|||
| 
								 | 
							
								            // TODO: what about style?
							 | 
						|||
| 
								 | 
							
								            hasTextNodes = true;
							 | 
						|||
| 
								 | 
							
								            combinedFormat &= node.getFormat();
							 | 
						|||
| 
								 | 
							
								            if (combinedFormat === 0) {
							 | 
						|||
| 
								 | 
							
								              break;
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        selection.format = hasTextNodes ? combinedFormat : 0;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, SELECTION_CHANGE_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// This is a work-around is mainly Chrome specific bug where if you select
							 | 
						|||
| 
								 | 
							
								// the contents of an empty block, you cannot easily unselect anything.
							 | 
						|||
| 
								 | 
							
								// This results in a tiny selection box that looks buggy/broken. This can
							 | 
						|||
| 
								 | 
							
								// also help other browsers when selection might "appear" lost, when it
							 | 
						|||
| 
								 | 
							
								// really isn't.
							 | 
						|||
| 
								 | 
							
								function onClick(event: PointerEvent, editor: LexicalEditor): void {
							 | 
						|||
| 
								 | 
							
								  updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								    const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								    const domSelection = getDOMSelection(editor._window);
							 | 
						|||
| 
								 | 
							
								    const lastSelection = $getPreviousSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (domSelection) {
							 | 
						|||
| 
								 | 
							
								      if ($isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								        const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								        const anchorNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        if (
							 | 
						|||
| 
								 | 
							
								          anchor.type === 'element' &&
							 | 
						|||
| 
								 | 
							
								          anchor.offset === 0 &&
							 | 
						|||
| 
								 | 
							
								          selection.isCollapsed() &&
							 | 
						|||
| 
								 | 
							
								          !$isRootNode(anchorNode) &&
							 | 
						|||
| 
								 | 
							
								          $getRoot().getChildrenSize() === 1 &&
							 | 
						|||
| 
								 | 
							
								          anchorNode.getTopLevelElementOrThrow().isEmpty() &&
							 | 
						|||
| 
								 | 
							
								          lastSelection !== null &&
							 | 
						|||
| 
								 | 
							
								          selection.is(lastSelection)
							 | 
						|||
| 
								 | 
							
								        ) {
							 | 
						|||
| 
								 | 
							
								          domSelection.removeAllRanges();
							 | 
						|||
| 
								 | 
							
								          selection.dirty = true;
							 | 
						|||
| 
								 | 
							
								        } else if (event.detail === 3 && !selection.isCollapsed()) {
							 | 
						|||
| 
								 | 
							
								          // Tripple click causing selection to overflow into the nearest element. In that
							 | 
						|||
| 
								 | 
							
								          // case visually it looks like a single element content is selected, focus node
							 | 
						|||
| 
								 | 
							
								          // is actually at the beginning of the next element (if present) and any manipulations
							 | 
						|||
| 
								 | 
							
								          // with selection (formatting) are affecting second element as well
							 | 
						|||
| 
								 | 
							
								          const focus = selection.focus;
							 | 
						|||
| 
								 | 
							
								          const focusNode = focus.getNode();
							 | 
						|||
| 
								 | 
							
								          if (anchorNode !== focusNode) {
							 | 
						|||
| 
								 | 
							
								            if ($isElementNode(anchorNode)) {
							 | 
						|||
| 
								 | 
							
								              anchorNode.select(0);
							 | 
						|||
| 
								 | 
							
								            } else {
							 | 
						|||
| 
								 | 
							
								              anchorNode.getParentOrThrow().select(0);
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      } else if (event.pointerType === 'touch') {
							 | 
						|||
| 
								 | 
							
								        // This is used to update the selection on touch devices when the user clicks on text after a
							 | 
						|||
| 
								 | 
							
								        // node selection. See isSelectionChangeFromMouseDown for the inverse
							 | 
						|||
| 
								 | 
							
								        const domAnchorNode = domSelection.anchorNode;
							 | 
						|||
| 
								 | 
							
								        if (domAnchorNode !== null) {
							 | 
						|||
| 
								 | 
							
								          const nodeType = domAnchorNode.nodeType;
							 | 
						|||
| 
								 | 
							
								          // If the user is attempting to click selection back onto text, then
							 | 
						|||
| 
								 | 
							
								          // we should attempt create a range selection.
							 | 
						|||
| 
								 | 
							
								          // When we click on an empty paragraph node or the end of a paragraph that ends
							 | 
						|||
| 
								 | 
							
								          // with an image/poll, the nodeType will be ELEMENT_NODE
							 | 
						|||
| 
								 | 
							
								          if (nodeType === DOM_ELEMENT_TYPE || nodeType === DOM_TEXT_TYPE) {
							 | 
						|||
| 
								 | 
							
								            const newSelection = $internalCreateRangeSelection(
							 | 
						|||
| 
								 | 
							
								              lastSelection,
							 | 
						|||
| 
								 | 
							
								              domSelection,
							 | 
						|||
| 
								 | 
							
								              editor,
							 | 
						|||
| 
								 | 
							
								              event,
							 | 
						|||
| 
								 | 
							
								            );
							 | 
						|||
| 
								 | 
							
								            $setSelection(newSelection);
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, CLICK_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onPointerDown(event: PointerEvent, editor: LexicalEditor) {
							 | 
						|||
| 
								 | 
							
								  // TODO implement text drag & drop
							 | 
						|||
| 
								 | 
							
								  const target = event.target;
							 | 
						|||
| 
								 | 
							
								  const pointerType = event.pointerType;
							 | 
						|||
| 
								 | 
							
								  if (target instanceof Node && pointerType !== 'touch') {
							 | 
						|||
| 
								 | 
							
								    updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								      // Drag & drop should not recompute selection until mouse up; otherwise the initially
							 | 
						|||
| 
								 | 
							
								      // selected content is lost.
							 | 
						|||
| 
								 | 
							
								      if (!$isSelectionCapturedInDecorator(target)) {
							 | 
						|||
| 
								 | 
							
								        isSelectionChangeFromMouseDown = true;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function getTargetRange(event: InputEvent): null | StaticRange {
							 | 
						|||
| 
								 | 
							
								  if (!event.getTargetRanges) {
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  const targetRanges = event.getTargetRanges();
							 | 
						|||
| 
								 | 
							
								  if (targetRanges.length === 0) {
							 | 
						|||
| 
								 | 
							
								    return null;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  return targetRanges[0];
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function $canRemoveText(
							 | 
						|||
| 
								 | 
							
								  anchorNode: TextNode | ElementNode,
							 | 
						|||
| 
								 | 
							
								  focusNode: TextNode | ElementNode,
							 | 
						|||
| 
								 | 
							
								): boolean {
							 | 
						|||
| 
								 | 
							
								  return (
							 | 
						|||
| 
								 | 
							
								    anchorNode !== focusNode ||
							 | 
						|||
| 
								 | 
							
								    $isElementNode(anchorNode) ||
							 | 
						|||
| 
								 | 
							
								    $isElementNode(focusNode) ||
							 | 
						|||
| 
								 | 
							
								    !anchorNode.isToken() ||
							 | 
						|||
| 
								 | 
							
								    !focusNode.isToken()
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function isPossiblyAndroidKeyPress(timeStamp: number): boolean {
							 | 
						|||
| 
								 | 
							
								  return (
							 | 
						|||
| 
								 | 
							
								    lastKeyCode === 'MediaLast' &&
							 | 
						|||
| 
								 | 
							
								    timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onBeforeInput(event: InputEvent, editor: LexicalEditor): void {
							 | 
						|||
| 
								 | 
							
								  const inputType = event.inputType;
							 | 
						|||
| 
								 | 
							
								  const targetRange = getTargetRange(event);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // We let the browser do its own thing for composition.
							 | 
						|||
| 
								 | 
							
								  if (
							 | 
						|||
| 
								 | 
							
								    inputType === 'deleteCompositionText' ||
							 | 
						|||
| 
								 | 
							
								    // If we're pasting in FF, we shouldn't get this event
							 | 
						|||
| 
								 | 
							
								    // as the `paste` event should have triggered, unless the
							 | 
						|||
| 
								 | 
							
								    // user has dom.event.clipboardevents.enabled disabled in
							 | 
						|||
| 
								 | 
							
								    // about:config. In that case, we need to process the
							 | 
						|||
| 
								 | 
							
								    // pasted content in the DOM mutation phase.
							 | 
						|||
| 
								 | 
							
								    (IS_FIREFOX && isFirefoxClipboardEvents(editor))
							 | 
						|||
| 
								 | 
							
								  ) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  } else if (inputType === 'insertCompositionText') {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								    const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (inputType === 'deleteContentBackward') {
							 | 
						|||
| 
								 | 
							
								      if (selection === null) {
							 | 
						|||
| 
								 | 
							
								        // Use previous selection
							 | 
						|||
| 
								 | 
							
								        const prevSelection = $getPreviousSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        if (!$isRangeSelection(prevSelection)) {
							 | 
						|||
| 
								 | 
							
								          return;
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        $setSelection(prevSelection.clone());
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if ($isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								        const isSelectionAnchorSameAsFocus =
							 | 
						|||
| 
								 | 
							
								          selection.anchor.key === selection.focus.key;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        if (
							 | 
						|||
| 
								 | 
							
								          isPossiblyAndroidKeyPress(event.timeStamp) &&
							 | 
						|||
| 
								 | 
							
								          editor.isComposing() &&
							 | 
						|||
| 
								 | 
							
								          isSelectionAnchorSameAsFocus
							 | 
						|||
| 
								 | 
							
								        ) {
							 | 
						|||
| 
								 | 
							
								          $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								          lastKeyDownTimeStamp = 0;
							 | 
						|||
| 
								 | 
							
								          // Fixes an Android bug where selection flickers when backspacing
							 | 
						|||
| 
								 | 
							
								          setTimeout(() => {
							 | 
						|||
| 
								 | 
							
								            updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								              $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								            });
							 | 
						|||
| 
								 | 
							
								          }, ANDROID_COMPOSITION_LATENCY);
							 | 
						|||
| 
								 | 
							
								          if ($isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								            const anchorNode = selection.anchor.getNode();
							 | 
						|||
| 
								 | 
							
								            anchorNode.markDirty();
							 | 
						|||
| 
								 | 
							
								            invariant(
							 | 
						|||
| 
								 | 
							
								              $isTextNode(anchorNode),
							 | 
						|||
| 
								 | 
							
								              'Anchor node must be a TextNode',
							 | 
						|||
| 
								 | 
							
								            );
							 | 
						|||
| 
								 | 
							
								            selection.style = anchorNode.getStyle();
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        } else {
							 | 
						|||
| 
								 | 
							
								          $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								          event.preventDefault();
							 | 
						|||
| 
								 | 
							
								          // Chromium Android at the moment seems to ignore the preventDefault
							 | 
						|||
| 
								 | 
							
								          // on 'deleteContentBackward' and still deletes the content. Which leads
							 | 
						|||
| 
								 | 
							
								          // to multiple deletions. So we let the browser handle the deletion in this case.
							 | 
						|||
| 
								 | 
							
								          const selectedNodeText = selection.anchor.getNode().getTextContent();
							 | 
						|||
| 
								 | 
							
								          const hasSelectedAllTextInNode =
							 | 
						|||
| 
								 | 
							
								            selection.anchor.offset === 0 &&
							 | 
						|||
| 
								 | 
							
								            selection.focus.offset === selectedNodeText.length;
							 | 
						|||
| 
								 | 
							
								          const shouldLetBrowserHandleDelete =
							 | 
						|||
| 
								 | 
							
								            IS_ANDROID_CHROME &&
							 | 
						|||
| 
								 | 
							
								            isSelectionAnchorSameAsFocus &&
							 | 
						|||
| 
								 | 
							
								            !hasSelectedAllTextInNode;
							 | 
						|||
| 
								 | 
							
								          if (!shouldLetBrowserHandleDelete) {
							 | 
						|||
| 
								 | 
							
								            dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (!$isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    const data = event.data;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // This represents the case when two beforeinput events are triggered at the same time (without a
							 | 
						|||
| 
								 | 
							
								    // full event loop ending at input). This happens with MacOS with the default keyboard settings,
							 | 
						|||
| 
								 | 
							
								    // a combination of autocorrection + autocapitalization.
							 | 
						|||
| 
								 | 
							
								    // Having Lexical run everything in controlled mode would fix the issue without additional code
							 | 
						|||
| 
								 | 
							
								    // but this would kill the massive performance win from the most common typing event.
							 | 
						|||
| 
								 | 
							
								    // Alternatively, when this happens we can prematurely update our EditorState based on the DOM
							 | 
						|||
| 
								 | 
							
								    // content, a job that would usually be the input event's responsibility.
							 | 
						|||
| 
								 | 
							
								    if (unprocessedBeforeInputData !== null) {
							 | 
						|||
| 
								 | 
							
								      $updateSelectedTextFromDOM(false, editor, unprocessedBeforeInputData);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (
							 | 
						|||
| 
								 | 
							
								      (!selection.dirty || unprocessedBeforeInputData !== null) &&
							 | 
						|||
| 
								 | 
							
								      selection.isCollapsed() &&
							 | 
						|||
| 
								 | 
							
								      !$isRootNode(selection.anchor.getNode()) &&
							 | 
						|||
| 
								 | 
							
								      targetRange !== null
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      selection.applyDOMRange(targetRange);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    unprocessedBeforeInputData = null;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								    const focus = selection.focus;
							 | 
						|||
| 
								 | 
							
								    const anchorNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								    const focusNode = focus.getNode();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (inputType === 'insertText' || inputType === 'insertTranspose') {
							 | 
						|||
| 
								 | 
							
								      if (data === '\n') {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								      } else if (data === DOUBLE_LINE_BREAK) {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								      } else if (data == null && event.dataTransfer) {
							 | 
						|||
| 
								 | 
							
								        // Gets around a Safari text replacement bug.
							 | 
						|||
| 
								 | 
							
								        const text = event.dataTransfer.getData('text/plain');
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        selection.insertRawText(text);
							 | 
						|||
| 
								 | 
							
								      } else if (
							 | 
						|||
| 
								 | 
							
								        data != null &&
							 | 
						|||
| 
								 | 
							
								        $shouldPreventDefaultAndInsertText(
							 | 
						|||
| 
								 | 
							
								          selection,
							 | 
						|||
| 
								 | 
							
								          targetRange,
							 | 
						|||
| 
								 | 
							
								          data,
							 | 
						|||
| 
								 | 
							
								          event.timeStamp,
							 | 
						|||
| 
								 | 
							
								          true,
							 | 
						|||
| 
								 | 
							
								        )
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
							 | 
						|||
| 
								 | 
							
								      } else {
							 | 
						|||
| 
								 | 
							
								        unprocessedBeforeInputData = data;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      lastBeforeInputInsertTextTimeStamp = event.timeStamp;
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // Prevent the browser from carrying out
							 | 
						|||
| 
								 | 
							
								    // the input event, so we can control the
							 | 
						|||
| 
								 | 
							
								    // output.
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    switch (inputType) {
							 | 
						|||
| 
								 | 
							
								      case 'insertFromYank':
							 | 
						|||
| 
								 | 
							
								      case 'insertFromDrop':
							 | 
						|||
| 
								 | 
							
								      case 'insertReplacementText': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'insertFromComposition': {
							 | 
						|||
| 
								 | 
							
								        // This is the end of composition
							 | 
						|||
| 
								 | 
							
								        $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'insertLineBreak': {
							 | 
						|||
| 
								 | 
							
								        // Used for Android
							 | 
						|||
| 
								 | 
							
								        $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'insertParagraph': {
							 | 
						|||
| 
								 | 
							
								        // Used for Android
							 | 
						|||
| 
								 | 
							
								        $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        // Safari does not provide the type "insertLineBreak".
							 | 
						|||
| 
								 | 
							
								        // So instead, we need to infer it from the keyboard event.
							 | 
						|||
| 
								 | 
							
								        // We do not apply this logic to iOS to allow newline auto-capitalization
							 | 
						|||
| 
								 | 
							
								        // work without creating linebreaks when pressing Enter
							 | 
						|||
| 
								 | 
							
								        if (isInsertLineBreak && !IS_IOS) {
							 | 
						|||
| 
								 | 
							
								          isInsertLineBreak = false;
							 | 
						|||
| 
								 | 
							
								          dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								        } else {
							 | 
						|||
| 
								 | 
							
								          dispatchCommand(editor, INSERT_PARAGRAPH_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'insertFromPaste':
							 | 
						|||
| 
								 | 
							
								      case 'insertFromPasteAsQuotation': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, PASTE_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteByComposition': {
							 | 
						|||
| 
								 | 
							
								        if ($canRemoveText(anchorNode, focusNode)) {
							 | 
						|||
| 
								 | 
							
								          dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								        }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteByDrag':
							 | 
						|||
| 
								 | 
							
								      case 'deleteByCut': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, REMOVE_TEXT_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteContent': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteWordBackward': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, DELETE_WORD_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteWordForward': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, DELETE_WORD_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteHardLineBackward':
							 | 
						|||
| 
								 | 
							
								      case 'deleteSoftLineBackward': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, DELETE_LINE_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'deleteContentForward':
							 | 
						|||
| 
								 | 
							
								      case 'deleteHardLineForward':
							 | 
						|||
| 
								 | 
							
								      case 'deleteSoftLineForward': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, DELETE_LINE_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'formatStrikeThrough': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'strikethrough');
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'formatBold': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'formatItalic': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'formatUnderline': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'historyUndo': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, UNDO_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      case 'historyRedo': {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, REDO_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								        break;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      default:
							 | 
						|||
| 
								 | 
							
								      // NO-OP
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onInput(event: InputEvent, editor: LexicalEditor): void {
							 | 
						|||
| 
								 | 
							
								  // We don't want the onInput to bubble, in the case of nested editors.
							 | 
						|||
| 
								 | 
							
								  event.stopPropagation();
							 | 
						|||
| 
								 | 
							
								  updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								    const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								    const data = event.data;
							 | 
						|||
| 
								 | 
							
								    const targetRange = getTargetRange(event);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (
							 | 
						|||
| 
								 | 
							
								      data != null &&
							 | 
						|||
| 
								 | 
							
								      $isRangeSelection(selection) &&
							 | 
						|||
| 
								 | 
							
								      $shouldPreventDefaultAndInsertText(
							 | 
						|||
| 
								 | 
							
								        selection,
							 | 
						|||
| 
								 | 
							
								        targetRange,
							 | 
						|||
| 
								 | 
							
								        data,
							 | 
						|||
| 
								 | 
							
								        event.timeStamp,
							 | 
						|||
| 
								 | 
							
								        false,
							 | 
						|||
| 
								 | 
							
								      )
							 | 
						|||
| 
								 | 
							
								    ) {
							 | 
						|||
| 
								 | 
							
								      // Given we're over-riding the default behavior, we will need
							 | 
						|||
| 
								 | 
							
								      // to ensure to disable composition before dispatching the
							 | 
						|||
| 
								 | 
							
								      // insertText command for when changing the sequence for FF.
							 | 
						|||
| 
								 | 
							
								      if (isFirefoxEndingComposition) {
							 | 
						|||
| 
								 | 
							
								        $onCompositionEndImpl(editor, data);
							 | 
						|||
| 
								 | 
							
								        isFirefoxEndingComposition = false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								      const anchorNode = anchor.getNode();
							 | 
						|||
| 
								 | 
							
								      const domSelection = getDOMSelection(editor._window);
							 | 
						|||
| 
								 | 
							
								      if (domSelection === null) {
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      const isBackward = selection.isBackward();
							 | 
						|||
| 
								 | 
							
								      const startOffset = isBackward
							 | 
						|||
| 
								 | 
							
								        ? selection.anchor.offset
							 | 
						|||
| 
								 | 
							
								        : selection.focus.offset;
							 | 
						|||
| 
								 | 
							
								      const endOffset = isBackward
							 | 
						|||
| 
								 | 
							
								        ? selection.focus.offset
							 | 
						|||
| 
								 | 
							
								        : selection.anchor.offset;
							 | 
						|||
| 
								 | 
							
								      // If the content is the same as inserted, then don't dispatch an insertion.
							 | 
						|||
| 
								 | 
							
								      // Given onInput doesn't take the current selection (it uses the previous)
							 | 
						|||
| 
								 | 
							
								      // we can compare that against what the DOM currently says.
							 | 
						|||
| 
								 | 
							
								      if (
							 | 
						|||
| 
								 | 
							
								        !CAN_USE_BEFORE_INPUT ||
							 | 
						|||
| 
								 | 
							
								        selection.isCollapsed() ||
							 | 
						|||
| 
								 | 
							
								        !$isTextNode(anchorNode) ||
							 | 
						|||
| 
								 | 
							
								        domSelection.anchorNode === null ||
							 | 
						|||
| 
								 | 
							
								        anchorNode.getTextContent().slice(0, startOffset) +
							 | 
						|||
| 
								 | 
							
								          data +
							 | 
						|||
| 
								 | 
							
								          anchorNode.getTextContent().slice(startOffset + endOffset) !==
							 | 
						|||
| 
								 | 
							
								          getAnchorTextFromDOM(domSelection.anchorNode)
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, CONTROLLED_TEXT_INSERTION_COMMAND, data);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      const textLength = data.length;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      // Another hack for FF, as it's possible that the IME is still
							 | 
						|||
| 
								 | 
							
								      // open, even though compositionend has already fired (sigh).
							 | 
						|||
| 
								 | 
							
								      if (
							 | 
						|||
| 
								 | 
							
								        IS_FIREFOX &&
							 | 
						|||
| 
								 | 
							
								        textLength > 1 &&
							 | 
						|||
| 
								 | 
							
								        event.inputType === 'insertCompositionText' &&
							 | 
						|||
| 
								 | 
							
								        !editor.isComposing()
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        selection.anchor.offset -= textLength;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      // This ensures consistency on Android.
							 | 
						|||
| 
								 | 
							
								      if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT && editor.isComposing()) {
							 | 
						|||
| 
								 | 
							
								        lastKeyDownTimeStamp = 0;
							 | 
						|||
| 
								 | 
							
								        $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      const characterData = data !== null ? data : undefined;
							 | 
						|||
| 
								 | 
							
								      $updateSelectedTextFromDOM(false, editor, characterData);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      // onInput always fires after onCompositionEnd for FF.
							 | 
						|||
| 
								 | 
							
								      if (isFirefoxEndingComposition) {
							 | 
						|||
| 
								 | 
							
								        $onCompositionEndImpl(editor, data || undefined);
							 | 
						|||
| 
								 | 
							
								        isFirefoxEndingComposition = false;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // Also flush any other mutations that might have occurred
							 | 
						|||
| 
								 | 
							
								    // since the change.
							 | 
						|||
| 
								 | 
							
								    $flushMutations();
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								  unprocessedBeforeInputData = null;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onCompositionStart(
							 | 
						|||
| 
								 | 
							
								  event: CompositionEvent,
							 | 
						|||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						|||
| 
								 | 
							
								): void {
							 | 
						|||
| 
								 | 
							
								  updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								    const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if ($isRangeSelection(selection) && !editor.isComposing()) {
							 | 
						|||
| 
								 | 
							
								      const anchor = selection.anchor;
							 | 
						|||
| 
								 | 
							
								      const node = selection.anchor.getNode();
							 | 
						|||
| 
								 | 
							
								      $setCompositionKey(anchor.key);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if (
							 | 
						|||
| 
								 | 
							
								        // If it has been 30ms since the last keydown, then we should
							 | 
						|||
| 
								 | 
							
								        // apply the empty space heuristic. We can't do this for Safari,
							 | 
						|||
| 
								 | 
							
								        // as the keydown fires after composition start.
							 | 
						|||
| 
								 | 
							
								        event.timeStamp < lastKeyDownTimeStamp + ANDROID_COMPOSITION_LATENCY ||
							 | 
						|||
| 
								 | 
							
								        // FF has issues around composing multibyte characters, so we also
							 | 
						|||
| 
								 | 
							
								        // need to invoke the empty space heuristic below.
							 | 
						|||
| 
								 | 
							
								        anchor.type === 'element' ||
							 | 
						|||
| 
								 | 
							
								        !selection.isCollapsed() ||
							 | 
						|||
| 
								 | 
							
								        ($isTextNode(node) && node.getStyle() !== selection.style)
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        // We insert a zero width character, ready for the composition
							 | 
						|||
| 
								 | 
							
								        // to get inserted into the new node we create. If
							 | 
						|||
| 
								 | 
							
								        // we don't do this, Safari will fail on us because
							 | 
						|||
| 
								 | 
							
								        // there is no text node matching the selection.
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(
							 | 
						|||
| 
								 | 
							
								          editor,
							 | 
						|||
| 
								 | 
							
								          CONTROLLED_TEXT_INSERTION_COMMAND,
							 | 
						|||
| 
								 | 
							
								          COMPOSITION_START_CHAR,
							 | 
						|||
| 
								 | 
							
								        );
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  });
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function $onCompositionEndImpl(editor: LexicalEditor, data?: string): void {
							 | 
						|||
| 
								 | 
							
								  const compositionKey = editor._compositionKey;
							 | 
						|||
| 
								 | 
							
								  $setCompositionKey(null);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // Handle termination of composition.
							 | 
						|||
| 
								 | 
							
								  if (compositionKey !== null && data != null) {
							 | 
						|||
| 
								 | 
							
								    // Composition can sometimes move to an adjacent DOM node when backspacing.
							 | 
						|||
| 
								 | 
							
								    // So check for the empty case.
							 | 
						|||
| 
								 | 
							
								    if (data === '') {
							 | 
						|||
| 
								 | 
							
								      const node = $getNodeByKey(compositionKey);
							 | 
						|||
| 
								 | 
							
								      const textNode = getDOMTextNode(editor.getElementByKey(compositionKey));
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if (
							 | 
						|||
| 
								 | 
							
								        textNode !== null &&
							 | 
						|||
| 
								 | 
							
								        textNode.nodeValue !== null &&
							 | 
						|||
| 
								 | 
							
								        $isTextNode(node)
							 | 
						|||
| 
								 | 
							
								      ) {
							 | 
						|||
| 
								 | 
							
								        $updateTextNodeFromDOMContent(
							 | 
						|||
| 
								 | 
							
								          node,
							 | 
						|||
| 
								 | 
							
								          textNode.nodeValue,
							 | 
						|||
| 
								 | 
							
								          null,
							 | 
						|||
| 
								 | 
							
								          null,
							 | 
						|||
| 
								 | 
							
								          true,
							 | 
						|||
| 
								 | 
							
								        );
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      return;
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    // Composition can sometimes be that of a new line. In which case, we need to
							 | 
						|||
| 
								 | 
							
								    // handle that accordingly.
							 | 
						|||
| 
								 | 
							
								    if (data[data.length - 1] === '\n') {
							 | 
						|||
| 
								 | 
							
								      const selection = $getSelection();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								      if ($isRangeSelection(selection)) {
							 | 
						|||
| 
								 | 
							
								        // If the last character is a line break, we also need to insert
							 | 
						|||
| 
								 | 
							
								        // a line break.
							 | 
						|||
| 
								 | 
							
								        const focus = selection.focus;
							 | 
						|||
| 
								 | 
							
								        selection.anchor.set(focus.key, focus.offset, focus.type);
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, KEY_ENTER_COMMAND, null);
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  $updateSelectedTextFromDOM(true, editor, data);
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onCompositionEnd(
							 | 
						|||
| 
								 | 
							
								  event: CompositionEvent,
							 | 
						|||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						|||
| 
								 | 
							
								): void {
							 | 
						|||
| 
								 | 
							
								  // Firefox fires onCompositionEnd before onInput, but Chrome/Webkit,
							 | 
						|||
| 
								 | 
							
								  // fire onInput before onCompositionEnd. To ensure the sequence works
							 | 
						|||
| 
								 | 
							
								  // like Chrome/Webkit we use the isFirefoxEndingComposition flag to
							 | 
						|||
| 
								 | 
							
								  // defer handling of onCompositionEnd in Firefox till we have processed
							 | 
						|||
| 
								 | 
							
								  // the logic in onInput.
							 | 
						|||
| 
								 | 
							
								  if (IS_FIREFOX) {
							 | 
						|||
| 
								 | 
							
								    isFirefoxEndingComposition = true;
							 | 
						|||
| 
								 | 
							
								  } else {
							 | 
						|||
| 
								 | 
							
								    updateEditor(editor, () => {
							 | 
						|||
| 
								 | 
							
								      $onCompositionEndImpl(editor, event.data);
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void {
							 | 
						|||
| 
								 | 
							
								  lastKeyDownTimeStamp = event.timeStamp;
							 | 
						|||
| 
								 | 
							
								  lastKeyCode = event.key;
							 | 
						|||
| 
								 | 
							
								  if (editor.isComposing()) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const {key, shiftKey, ctrlKey, metaKey, altKey} = event;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (key == null) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (isMoveForward(key, ctrlKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ARROW_RIGHT_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isMoveToEnd(key, ctrlKey, shiftKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, MOVE_TO_END, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isMoveBackward(key, ctrlKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ARROW_LEFT_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isMoveToStart(key, ctrlKey, shiftKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, MOVE_TO_START, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isMoveUp(key, ctrlKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ARROW_UP_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isMoveDown(key, ctrlKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ARROW_DOWN_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isLineBreak(key, shiftKey)) {
							 | 
						|||
| 
								 | 
							
								    isInsertLineBreak = true;
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ENTER_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isSpace(key)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_SPACE_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isOpenLineBreak(key, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    isInsertLineBreak = true;
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, INSERT_LINE_BREAK_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								  } else if (isParagraph(key, shiftKey)) {
							 | 
						|||
| 
								 | 
							
								    isInsertLineBreak = false;
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ENTER_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteBackward(key, altKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    if (isBackspace(key)) {
							 | 
						|||
| 
								 | 
							
								      dispatchCommand(editor, KEY_BACKSPACE_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      event.preventDefault();
							 | 
						|||
| 
								 | 
							
								      dispatchCommand(editor, DELETE_CHARACTER_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  } else if (isEscape(key)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_ESCAPE_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteForward(key, ctrlKey, shiftKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    if (isDelete(key)) {
							 | 
						|||
| 
								 | 
							
								      dispatchCommand(editor, KEY_DELETE_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								    } else {
							 | 
						|||
| 
								 | 
							
								      event.preventDefault();
							 | 
						|||
| 
								 | 
							
								      dispatchCommand(editor, DELETE_CHARACTER_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteWordBackward(key, altKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, DELETE_WORD_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteWordForward(key, altKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, DELETE_WORD_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteLineBackward(key, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, DELETE_LINE_COMMAND, true);
							 | 
						|||
| 
								 | 
							
								  } else if (isDeleteLineForward(key, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, DELETE_LINE_COMMAND, false);
							 | 
						|||
| 
								 | 
							
								  } else if (isBold(key, altKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold');
							 | 
						|||
| 
								 | 
							
								  } else if (isUnderline(key, altKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline');
							 | 
						|||
| 
								 | 
							
								  } else if (isItalic(key, altKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic');
							 | 
						|||
| 
								 | 
							
								  } else if (isTab(key, altKey, ctrlKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_TAB_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  } else if (isUndo(key, shiftKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, UNDO_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								  } else if (isRedo(key, shiftKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								    event.preventDefault();
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, REDO_COMMAND, undefined);
							 | 
						|||
| 
								 | 
							
								  } else {
							 | 
						|||
| 
								 | 
							
								    const prevSelection = editor._editorState._selection;
							 | 
						|||
| 
								 | 
							
								    if ($isNodeSelection(prevSelection)) {
							 | 
						|||
| 
								 | 
							
								      if (isCopy(key, shiftKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, COPY_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								      } else if (isCut(key, shiftKey, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, CUT_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								      } else if (isSelectAll(key, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								        event.preventDefault();
							 | 
						|||
| 
								 | 
							
								        dispatchCommand(editor, SELECT_ALL_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      // FF does it well (no need to override behavior)
							 | 
						|||
| 
								 | 
							
								    } else if (!IS_FIREFOX && isSelectAll(key, metaKey, ctrlKey)) {
							 | 
						|||
| 
								 | 
							
								      event.preventDefault();
							 | 
						|||
| 
								 | 
							
								      dispatchCommand(editor, SELECT_ALL_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (isModifier(ctrlKey, shiftKey, altKey, metaKey)) {
							 | 
						|||
| 
								 | 
							
								    dispatchCommand(editor, KEY_MODIFIER_COMMAND, event);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function getRootElementRemoveHandles(
							 | 
						|||
| 
								 | 
							
								  rootElement: HTMLElement,
							 | 
						|||
| 
								 | 
							
								): RootElementRemoveHandles {
							 | 
						|||
| 
								 | 
							
								  // @ts-expect-error: internal field
							 | 
						|||
| 
								 | 
							
								  let eventHandles = rootElement.__lexicalEventHandles;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (eventHandles === undefined) {
							 | 
						|||
| 
								 | 
							
								    eventHandles = [];
							 | 
						|||
| 
								 | 
							
								    // @ts-expect-error: internal field
							 | 
						|||
| 
								 | 
							
								    rootElement.__lexicalEventHandles = eventHandles;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  return eventHandles;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								// Mapping root editors to their active nested editors, contains nested editors
							 | 
						|||
| 
								 | 
							
								// mapping only, so if root editor is selected map will have no reference to free up memory
							 | 
						|||
| 
								 | 
							
								const activeNestedEditorsMap: Map<string, LexicalEditor> = new Map();
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function onDocumentSelectionChange(event: Event): void {
							 | 
						|||
| 
								 | 
							
								  const target = event.target as null | Element | Document;
							 | 
						|||
| 
								 | 
							
								  const targetWindow =
							 | 
						|||
| 
								 | 
							
								    target == null
							 | 
						|||
| 
								 | 
							
								      ? null
							 | 
						|||
| 
								 | 
							
								      : target.nodeType === 9
							 | 
						|||
| 
								 | 
							
								      ? (target as Document).defaultView
							 | 
						|||
| 
								 | 
							
								      : (target as Element).ownerDocument.defaultView;
							 | 
						|||
| 
								 | 
							
								  const domSelection = getDOMSelection(targetWindow);
							 | 
						|||
| 
								 | 
							
								  if (domSelection === null) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  const nextActiveEditor = getNearestEditorFromDOMNode(domSelection.anchorNode);
							 | 
						|||
| 
								 | 
							
								  if (nextActiveEditor === null) {
							 | 
						|||
| 
								 | 
							
								    return;
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (isSelectionChangeFromMouseDown) {
							 | 
						|||
| 
								 | 
							
								    isSelectionChangeFromMouseDown = false;
							 | 
						|||
| 
								 | 
							
								    updateEditor(nextActiveEditor, () => {
							 | 
						|||
| 
								 | 
							
								      const lastSelection = $getPreviousSelection();
							 | 
						|||
| 
								 | 
							
								      const domAnchorNode = domSelection.anchorNode;
							 | 
						|||
| 
								 | 
							
								      if (domAnchorNode === null) {
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      const nodeType = domAnchorNode.nodeType;
							 | 
						|||
| 
								 | 
							
								      // If the user is attempting to click selection back onto text, then
							 | 
						|||
| 
								 | 
							
								      // we should attempt create a range selection.
							 | 
						|||
| 
								 | 
							
								      // When we click on an empty paragraph node or the end of a paragraph that ends
							 | 
						|||
| 
								 | 
							
								      // with an image/poll, the nodeType will be ELEMENT_NODE
							 | 
						|||
| 
								 | 
							
								      if (nodeType !== DOM_ELEMENT_TYPE && nodeType !== DOM_TEXT_TYPE) {
							 | 
						|||
| 
								 | 
							
								        return;
							 | 
						|||
| 
								 | 
							
								      }
							 | 
						|||
| 
								 | 
							
								      const newSelection = $internalCreateRangeSelection(
							 | 
						|||
| 
								 | 
							
								        lastSelection,
							 | 
						|||
| 
								 | 
							
								        domSelection,
							 | 
						|||
| 
								 | 
							
								        nextActiveEditor,
							 | 
						|||
| 
								 | 
							
								        event,
							 | 
						|||
| 
								 | 
							
								      );
							 | 
						|||
| 
								 | 
							
								      $setSelection(newSelection);
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // When editor receives selection change event, we're checking if
							 | 
						|||
| 
								 | 
							
								  // it has any sibling editors (within same parent editor) that were active
							 | 
						|||
| 
								 | 
							
								  // before, and trigger selection change on it to nullify selection.
							 | 
						|||
| 
								 | 
							
								  const editors = getEditorsToPropagate(nextActiveEditor);
							 | 
						|||
| 
								 | 
							
								  const rootEditor = editors[editors.length - 1];
							 | 
						|||
| 
								 | 
							
								  const rootEditorKey = rootEditor._key;
							 | 
						|||
| 
								 | 
							
								  const activeNestedEditor = activeNestedEditorsMap.get(rootEditorKey);
							 | 
						|||
| 
								 | 
							
								  const prevActiveEditor = activeNestedEditor || rootEditor;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (prevActiveEditor !== nextActiveEditor) {
							 | 
						|||
| 
								 | 
							
								    onSelectionChange(domSelection, prevActiveEditor, false);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  onSelectionChange(domSelection, nextActiveEditor, true);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // If newly selected editor is nested, then add it to the map, clean map otherwise
							 | 
						|||
| 
								 | 
							
								  if (nextActiveEditor !== rootEditor) {
							 | 
						|||
| 
								 | 
							
								    activeNestedEditorsMap.set(rootEditorKey, nextActiveEditor);
							 | 
						|||
| 
								 | 
							
								  } else if (activeNestedEditor) {
							 | 
						|||
| 
								 | 
							
								    activeNestedEditorsMap.delete(rootEditorKey);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function stopLexicalPropagation(event: Event): void {
							 | 
						|||
| 
								 | 
							
								  // We attach a special property to ensure the same event doesn't re-fire
							 | 
						|||
| 
								 | 
							
								  // for parent editors.
							 | 
						|||
| 
								 | 
							
								  // @ts-ignore
							 | 
						|||
| 
								 | 
							
								  event._lexicalHandled = true;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function hasStoppedLexicalPropagation(event: Event): boolean {
							 | 
						|||
| 
								 | 
							
								  // @ts-ignore
							 | 
						|||
| 
								 | 
							
								  const stopped = event._lexicalHandled === true;
							 | 
						|||
| 
								 | 
							
								  return stopped;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								export type EventHandler = (event: Event, editor: LexicalEditor) => void;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								export function addRootElementEvents(
							 | 
						|||
| 
								 | 
							
								  rootElement: HTMLElement,
							 | 
						|||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						|||
| 
								 | 
							
								): void {
							 | 
						|||
| 
								 | 
							
								  // We only want to have a single global selectionchange event handler, shared
							 | 
						|||
| 
								 | 
							
								  // between all editor instances.
							 | 
						|||
| 
								 | 
							
								  const doc = rootElement.ownerDocument;
							 | 
						|||
| 
								 | 
							
								  const documentRootElementsCount = rootElementsRegistered.get(doc);
							 | 
						|||
| 
								 | 
							
								  if (
							 | 
						|||
| 
								 | 
							
								    documentRootElementsCount === undefined ||
							 | 
						|||
| 
								 | 
							
								    documentRootElementsCount < 1
							 | 
						|||
| 
								 | 
							
								  ) {
							 | 
						|||
| 
								 | 
							
								    doc.addEventListener('selectionchange', onDocumentSelectionChange);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								  rootElementsRegistered.set(doc, (documentRootElementsCount || 0) + 1);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // @ts-expect-error: internal field
							 | 
						|||
| 
								 | 
							
								  rootElement.__lexicalEditor = editor;
							 | 
						|||
| 
								 | 
							
								  const removeHandles = getRootElementRemoveHandles(rootElement);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  for (let i = 0; i < rootElementEvents.length; i++) {
							 | 
						|||
| 
								 | 
							
								    const [eventName, onEvent] = rootElementEvents[i];
							 | 
						|||
| 
								 | 
							
								    const eventHandler =
							 | 
						|||
| 
								 | 
							
								      typeof onEvent === 'function'
							 | 
						|||
| 
								 | 
							
								        ? (event: Event) => {
							 | 
						|||
| 
								 | 
							
								            if (hasStoppedLexicalPropagation(event)) {
							 | 
						|||
| 
								 | 
							
								              return;
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								            stopLexicalPropagation(event);
							 | 
						|||
| 
								 | 
							
								            if (editor.isEditable() || eventName === 'click') {
							 | 
						|||
| 
								 | 
							
								              onEvent(event, editor);
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          }
							 | 
						|||
| 
								 | 
							
								        : (event: Event) => {
							 | 
						|||
| 
								 | 
							
								            if (hasStoppedLexicalPropagation(event)) {
							 | 
						|||
| 
								 | 
							
								              return;
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								            stopLexicalPropagation(event);
							 | 
						|||
| 
								 | 
							
								            const isEditable = editor.isEditable();
							 | 
						|||
| 
								 | 
							
								            switch (eventName) {
							 | 
						|||
| 
								 | 
							
								              case 'cut':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, CUT_COMMAND, event as ClipboardEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'copy':
							 | 
						|||
| 
								 | 
							
								                return dispatchCommand(
							 | 
						|||
| 
								 | 
							
								                  editor,
							 | 
						|||
| 
								 | 
							
								                  COPY_COMMAND,
							 | 
						|||
| 
								 | 
							
								                  event as ClipboardEvent,
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'paste':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(
							 | 
						|||
| 
								 | 
							
								                    editor,
							 | 
						|||
| 
								 | 
							
								                    PASTE_COMMAND,
							 | 
						|||
| 
								 | 
							
								                    event as ClipboardEvent,
							 | 
						|||
| 
								 | 
							
								                  )
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'dragstart':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, DRAGSTART_COMMAND, event as DragEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'dragover':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, DRAGOVER_COMMAND, event as DragEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'dragend':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, DRAGEND_COMMAND, event as DragEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'focus':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, FOCUS_COMMAND, event as FocusEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'blur': {
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, BLUR_COMMAND, event as FocusEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								              }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								              case 'drop':
							 | 
						|||
| 
								 | 
							
								                return (
							 | 
						|||
| 
								 | 
							
								                  isEditable &&
							 | 
						|||
| 
								 | 
							
								                  dispatchCommand(editor, DROP_COMMAND, event as DragEvent)
							 | 
						|||
| 
								 | 
							
								                );
							 | 
						|||
| 
								 | 
							
								            }
							 | 
						|||
| 
								 | 
							
								          };
							 | 
						|||
| 
								 | 
							
								    rootElement.addEventListener(eventName, eventHandler);
							 | 
						|||
| 
								 | 
							
								    removeHandles.push(() => {
							 | 
						|||
| 
								 | 
							
								      rootElement.removeEventListener(eventName, eventHandler);
							 | 
						|||
| 
								 | 
							
								    });
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								export function removeRootElementEvents(rootElement: HTMLElement): void {
							 | 
						|||
| 
								 | 
							
								  const doc = rootElement.ownerDocument;
							 | 
						|||
| 
								 | 
							
								  const documentRootElementsCount = rootElementsRegistered.get(doc);
							 | 
						|||
| 
								 | 
							
								  invariant(
							 | 
						|||
| 
								 | 
							
								    documentRootElementsCount !== undefined,
							 | 
						|||
| 
								 | 
							
								    'Root element not registered',
							 | 
						|||
| 
								 | 
							
								  );
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // We only want to have a single global selectionchange event handler, shared
							 | 
						|||
| 
								 | 
							
								  // between all editor instances.
							 | 
						|||
| 
								 | 
							
								  const newCount = documentRootElementsCount - 1;
							 | 
						|||
| 
								 | 
							
								  invariant(newCount >= 0, 'Root element count less than 0');
							 | 
						|||
| 
								 | 
							
								  rootElementsRegistered.set(doc, newCount);
							 | 
						|||
| 
								 | 
							
								  if (newCount === 0) {
							 | 
						|||
| 
								 | 
							
								    doc.removeEventListener('selectionchange', onDocumentSelectionChange);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const editor = getEditorPropertyFromDOMNode(rootElement);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  if (isLexicalEditor(editor)) {
							 | 
						|||
| 
								 | 
							
								    cleanActiveNestedEditorsMap(editor);
							 | 
						|||
| 
								 | 
							
								    // @ts-expect-error: internal field
							 | 
						|||
| 
								 | 
							
								    rootElement.__lexicalEditor = null;
							 | 
						|||
| 
								 | 
							
								  } else if (editor) {
							 | 
						|||
| 
								 | 
							
								    invariant(
							 | 
						|||
| 
								 | 
							
								      false,
							 | 
						|||
| 
								 | 
							
								      'Attempted to remove event handlers from a node that does not belong to this build of Lexical',
							 | 
						|||
| 
								 | 
							
								    );
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  const removeHandles = getRootElementRemoveHandles(rootElement);
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  for (let i = 0; i < removeHandles.length; i++) {
							 | 
						|||
| 
								 | 
							
								    removeHandles[i]();
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								  // @ts-expect-error: internal field
							 | 
						|||
| 
								 | 
							
								  rootElement.__lexicalEventHandles = [];
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								function cleanActiveNestedEditorsMap(editor: LexicalEditor) {
							 | 
						|||
| 
								 | 
							
								  if (editor._parentEditor !== null) {
							 | 
						|||
| 
								 | 
							
								    // For nested editor cleanup map if this editor was marked as active
							 | 
						|||
| 
								 | 
							
								    const editors = getEditorsToPropagate(editor);
							 | 
						|||
| 
								 | 
							
								    const rootEditor = editors[editors.length - 1];
							 | 
						|||
| 
								 | 
							
								    const rootEditorKey = rootEditor._key;
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								    if (activeNestedEditorsMap.get(rootEditorKey) === editor) {
							 | 
						|||
| 
								 | 
							
								      activeNestedEditorsMap.delete(rootEditorKey);
							 | 
						|||
| 
								 | 
							
								    }
							 | 
						|||
| 
								 | 
							
								  } else {
							 | 
						|||
| 
								 | 
							
								    // For top-level editors cleanup map
							 | 
						|||
| 
								 | 
							
								    activeNestedEditorsMap.delete(editor._key);
							 | 
						|||
| 
								 | 
							
								  }
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								export function markSelectionChangeFromDOMUpdate(): void {
							 | 
						|||
| 
								 | 
							
								  isSelectionChangeFromDOMUpdate = true;
							 | 
						|||
| 
								 | 
							
								}
							 | 
						|||
| 
								 | 
							
								
							 | 
						|||
| 
								 | 
							
								export function markCollapsedSelectionFormat(
							 | 
						|||
| 
								 | 
							
								  format: number,
							 | 
						|||
| 
								 | 
							
								  style: string,
							 | 
						|||
| 
								 | 
							
								  offset: number,
							 | 
						|||
| 
								 | 
							
								  key: NodeKey,
							 | 
						|||
| 
								 | 
							
								  timeStamp: number,
							 | 
						|||
| 
								 | 
							
								): void {
							 | 
						|||
| 
								 | 
							
								  collapsedSelectionFormat = [format, style, offset, key, timeStamp];
							 | 
						|||
| 
								 | 
							
								}
							 |