1789 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			1789 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
|  | /** | ||
|  |  * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|  |  * | ||
|  |  * This source code is licensed under the MIT license found in the | ||
|  |  * LICENSE file in the root directory of this source tree. | ||
|  |  * | ||
|  |  */ | ||
|  | 
 | ||
|  | import type { | ||
|  |   CommandPayloadType, | ||
|  |   EditorConfig, | ||
|  |   EditorThemeClasses, | ||
|  |   Klass, | ||
|  |   LexicalCommand, | ||
|  |   MutatedNodes, | ||
|  |   MutationListeners, | ||
|  |   NodeMutation, | ||
|  |   RegisteredNode, | ||
|  |   RegisteredNodes, | ||
|  |   Spread, | ||
|  | } from './LexicalEditor'; | ||
|  | import type {EditorState} from './LexicalEditorState'; | ||
|  | import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode'; | ||
|  | import type { | ||
|  |   BaseSelection, | ||
|  |   PointType, | ||
|  |   RangeSelection, | ||
|  | } from './LexicalSelection'; | ||
|  | import type {RootNode} from './nodes/LexicalRootNode'; | ||
|  | import type {TextFormatType, TextNode} from './nodes/LexicalTextNode'; | ||
|  | 
 | ||
|  | import {CAN_USE_DOM} from 'lexical/shared/canUseDOM'; | ||
|  | import {IS_APPLE, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI} from 'lexical/shared/environment'; | ||
|  | import invariant from 'lexical/shared/invariant'; | ||
|  | import normalizeClassNames from 'lexical/shared/normalizeClassNames'; | ||
|  | 
 | ||
|  | import { | ||
|  |   $createTextNode, | ||
|  |   $getPreviousSelection, | ||
|  |   $getSelection, | ||
|  |   $isDecoratorNode, | ||
|  |   $isElementNode, | ||
|  |   $isLineBreakNode, | ||
|  |   $isRangeSelection, | ||
|  |   $isRootNode, | ||
|  |   $isTextNode, | ||
|  |   DecoratorNode, | ||
|  |   ElementNode, | ||
|  |   LineBreakNode, | ||
|  | } from '.'; | ||
|  | import { | ||
|  |   COMPOSITION_SUFFIX, | ||
|  |   DOM_TEXT_TYPE, | ||
|  |   HAS_DIRTY_NODES, | ||
|  |   LTR_REGEX, | ||
|  |   RTL_REGEX, | ||
|  |   TEXT_TYPE_TO_FORMAT, | ||
|  | } from './LexicalConstants'; | ||
|  | import {LexicalEditor} from './LexicalEditor'; | ||
|  | import {$flushRootMutations} from './LexicalMutations'; | ||
|  | import {$normalizeSelection} from './LexicalNormalization'; | ||
|  | import { | ||
|  |   errorOnInfiniteTransforms, | ||
|  |   errorOnReadOnly, | ||
|  |   getActiveEditor, | ||
|  |   getActiveEditorState, | ||
|  |   internalGetActiveEditorState, | ||
|  |   isCurrentlyReadOnlyMode, | ||
|  |   triggerCommandListeners, | ||
|  |   updateEditor, | ||
|  | } from './LexicalUpdates'; | ||
|  | 
 | ||
|  | export const emptyFunction = () => { | ||
|  |   return; | ||
|  | }; | ||
|  | 
 | ||
|  | let keyCounter = 1; | ||
|  | 
 | ||
|  | export function resetRandomKey(): void { | ||
|  |   keyCounter = 1; | ||
|  | } | ||
|  | 
 | ||
|  | export function generateRandomKey(): string { | ||
|  |   return '' + keyCounter++; | ||
|  | } | ||
|  | 
 | ||
|  | export function getRegisteredNodeOrThrow( | ||
|  |   editor: LexicalEditor, | ||
|  |   nodeType: string, | ||
|  | ): RegisteredNode { | ||
|  |   const registeredNode = editor._nodes.get(nodeType); | ||
|  |   if (registeredNode === undefined) { | ||
|  |     invariant(false, 'registeredNode: Type %s not found', nodeType); | ||
|  |   } | ||
|  |   return registeredNode; | ||
|  | } | ||
|  | 
 | ||
|  | export const isArray = Array.isArray; | ||
|  | 
 | ||
|  | export const scheduleMicroTask: (fn: () => void) => void = | ||
|  |   typeof queueMicrotask === 'function' | ||
|  |     ? queueMicrotask | ||
|  |     : (fn) => { | ||
|  |         // No window prefix intended (#1400)
 | ||
|  |         Promise.resolve().then(fn); | ||
|  |       }; | ||
|  | 
 | ||
|  | export function $isSelectionCapturedInDecorator(node: Node): boolean { | ||
|  |   return $isDecoratorNode($getNearestNodeFromDOMNode(node)); | ||
|  | } | ||
|  | 
 | ||
|  | export function isSelectionCapturedInDecoratorInput(anchorDOM: Node): boolean { | ||
|  |   const activeElement = document.activeElement as HTMLElement; | ||
|  | 
 | ||
|  |   if (activeElement === null) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   const nodeName = activeElement.nodeName; | ||
|  | 
 | ||
|  |   return ( | ||
|  |     $isDecoratorNode($getNearestNodeFromDOMNode(anchorDOM)) && | ||
|  |     (nodeName === 'INPUT' || | ||
|  |       nodeName === 'TEXTAREA' || | ||
|  |       (activeElement.contentEditable === 'true' && | ||
|  |         getEditorPropertyFromDOMNode(activeElement) == null)) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isSelectionWithinEditor( | ||
|  |   editor: LexicalEditor, | ||
|  |   anchorDOM: null | Node, | ||
|  |   focusDOM: null | Node, | ||
|  | ): boolean { | ||
|  |   const rootElement = editor.getRootElement(); | ||
|  |   try { | ||
|  |     return ( | ||
|  |       rootElement !== null && | ||
|  |       rootElement.contains(anchorDOM) && | ||
|  |       rootElement.contains(focusDOM) && | ||
|  |       // Ignore if selection is within nested editor
 | ||
|  |       anchorDOM !== null && | ||
|  |       !isSelectionCapturedInDecoratorInput(anchorDOM as Node) && | ||
|  |       getNearestEditorFromDOMNode(anchorDOM) === editor | ||
|  |     ); | ||
|  |   } catch (error) { | ||
|  |     return false; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @returns true if the given argument is a LexicalEditor instance from this build of Lexical | ||
|  |  */ | ||
|  | export function isLexicalEditor(editor: unknown): editor is LexicalEditor { | ||
|  |   // Check instanceof to prevent issues with multiple embedded Lexical installations
 | ||
|  |   return editor instanceof LexicalEditor; | ||
|  | } | ||
|  | 
 | ||
|  | export function getNearestEditorFromDOMNode( | ||
|  |   node: Node | null, | ||
|  | ): LexicalEditor | null { | ||
|  |   let currentNode = node; | ||
|  |   while (currentNode != null) { | ||
|  |     const editor = getEditorPropertyFromDOMNode(currentNode); | ||
|  |     if (isLexicalEditor(editor)) { | ||
|  |       return editor; | ||
|  |     } | ||
|  |     currentNode = getParentElement(currentNode); | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | /** @internal */ | ||
|  | export function getEditorPropertyFromDOMNode(node: Node | null): unknown { | ||
|  |   // @ts-expect-error: internal field
 | ||
|  |   return node ? node.__lexicalEditor : null; | ||
|  | } | ||
|  | 
 | ||
|  | export function getTextDirection(text: string): 'ltr' | 'rtl' | null { | ||
|  |   if (RTL_REGEX.test(text)) { | ||
|  |     return 'rtl'; | ||
|  |   } | ||
|  |   if (LTR_REGEX.test(text)) { | ||
|  |     return 'ltr'; | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function $isTokenOrSegmented(node: TextNode): boolean { | ||
|  |   return node.isToken() || node.isSegmented(); | ||
|  | } | ||
|  | 
 | ||
|  | function isDOMNodeLexicalTextNode(node: Node): node is Text { | ||
|  |   return node.nodeType === DOM_TEXT_TYPE; | ||
|  | } | ||
|  | 
 | ||
|  | export function getDOMTextNode(element: Node | null): Text | null { | ||
|  |   let node = element; | ||
|  |   while (node != null) { | ||
|  |     if (isDOMNodeLexicalTextNode(node)) { | ||
|  |       return node; | ||
|  |     } | ||
|  |     node = node.firstChild; | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function toggleTextFormatType( | ||
|  |   format: number, | ||
|  |   type: TextFormatType, | ||
|  |   alignWithFormat: null | number, | ||
|  | ): number { | ||
|  |   const activeFormat = TEXT_TYPE_TO_FORMAT[type]; | ||
|  |   if ( | ||
|  |     alignWithFormat !== null && | ||
|  |     (format & activeFormat) === (alignWithFormat & activeFormat) | ||
|  |   ) { | ||
|  |     return format; | ||
|  |   } | ||
|  |   let newFormat = format ^ activeFormat; | ||
|  |   if (type === 'subscript') { | ||
|  |     newFormat &= ~TEXT_TYPE_TO_FORMAT.superscript; | ||
|  |   } else if (type === 'superscript') { | ||
|  |     newFormat &= ~TEXT_TYPE_TO_FORMAT.subscript; | ||
|  |   } | ||
|  |   return newFormat; | ||
|  | } | ||
|  | 
 | ||
|  | export function $isLeafNode( | ||
|  |   node: LexicalNode | null | undefined, | ||
|  | ): node is TextNode | LineBreakNode | DecoratorNode<unknown> { | ||
|  |   return $isTextNode(node) || $isLineBreakNode(node) || $isDecoratorNode(node); | ||
|  | } | ||
|  | 
 | ||
|  | export function $setNodeKey( | ||
|  |   node: LexicalNode, | ||
|  |   existingKey: NodeKey | null | undefined, | ||
|  | ): void { | ||
|  |   if (existingKey != null) { | ||
|  |     if (__DEV__) { | ||
|  |       errorOnNodeKeyConstructorMismatch(node, existingKey); | ||
|  |     } | ||
|  |     node.__key = existingKey; | ||
|  |     return; | ||
|  |   } | ||
|  |   errorOnReadOnly(); | ||
|  |   errorOnInfiniteTransforms(); | ||
|  |   const editor = getActiveEditor(); | ||
|  |   const editorState = getActiveEditorState(); | ||
|  |   const key = generateRandomKey(); | ||
|  |   editorState._nodeMap.set(key, node); | ||
|  |   // TODO Split this function into leaf/element
 | ||
|  |   if ($isElementNode(node)) { | ||
|  |     editor._dirtyElements.set(key, true); | ||
|  |   } else { | ||
|  |     editor._dirtyLeaves.add(key); | ||
|  |   } | ||
|  |   editor._cloneNotNeeded.add(key); | ||
|  |   editor._dirtyType = HAS_DIRTY_NODES; | ||
|  |   node.__key = key; | ||
|  | } | ||
|  | 
 | ||
|  | function errorOnNodeKeyConstructorMismatch( | ||
|  |   node: LexicalNode, | ||
|  |   existingKey: NodeKey, | ||
|  | ) { | ||
|  |   const editorState = internalGetActiveEditorState(); | ||
|  |   if (!editorState) { | ||
|  |     // tests expect to be able to do this kind of clone without an active editor state
 | ||
|  |     return; | ||
|  |   } | ||
|  |   const existingNode = editorState._nodeMap.get(existingKey); | ||
|  |   if (existingNode && existingNode.constructor !== node.constructor) { | ||
|  |     // Lifted condition to if statement because the inverted logic is a bit confusing
 | ||
|  |     if (node.constructor.name !== existingNode.constructor.name) { | ||
|  |       invariant( | ||
|  |         false, | ||
|  |         'Lexical node with constructor %s attempted to re-use key from node in active editor state with constructor %s. Keys must not be re-used when the type is changed.', | ||
|  |         node.constructor.name, | ||
|  |         existingNode.constructor.name, | ||
|  |       ); | ||
|  |     } else { | ||
|  |       invariant( | ||
|  |         false, | ||
|  |         'Lexical node with constructor %s attempted to re-use key from node in active editor state with different constructor with the same name (possibly due to invalid Hot Module Replacement). Keys must not be re-used when the type is changed.', | ||
|  |         node.constructor.name, | ||
|  |       ); | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | type IntentionallyMarkedAsDirtyElement = boolean; | ||
|  | 
 | ||
|  | function internalMarkParentElementsAsDirty( | ||
|  |   parentKey: NodeKey, | ||
|  |   nodeMap: NodeMap, | ||
|  |   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>, | ||
|  | ): void { | ||
|  |   let nextParentKey: string | null = parentKey; | ||
|  |   while (nextParentKey !== null) { | ||
|  |     if (dirtyElements.has(nextParentKey)) { | ||
|  |       return; | ||
|  |     } | ||
|  |     const node = nodeMap.get(nextParentKey); | ||
|  |     if (node === undefined) { | ||
|  |       break; | ||
|  |     } | ||
|  |     dirtyElements.set(nextParentKey, false); | ||
|  |     nextParentKey = node.__parent; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | // TODO #6031 this function or their callers have to adjust selection (i.e. insertBefore)
 | ||
|  | export function removeFromParent(node: LexicalNode): void { | ||
|  |   const oldParent = node.getParent(); | ||
|  |   if (oldParent !== null) { | ||
|  |     const writableNode = node.getWritable(); | ||
|  |     const writableParent = oldParent.getWritable(); | ||
|  |     const prevSibling = node.getPreviousSibling(); | ||
|  |     const nextSibling = node.getNextSibling(); | ||
|  |     // TODO: this function duplicates a bunch of operations, can be simplified.
 | ||
|  |     if (prevSibling === null) { | ||
|  |       if (nextSibling !== null) { | ||
|  |         const writableNextSibling = nextSibling.getWritable(); | ||
|  |         writableParent.__first = nextSibling.__key; | ||
|  |         writableNextSibling.__prev = null; | ||
|  |       } else { | ||
|  |         writableParent.__first = null; | ||
|  |       } | ||
|  |     } else { | ||
|  |       const writablePrevSibling = prevSibling.getWritable(); | ||
|  |       if (nextSibling !== null) { | ||
|  |         const writableNextSibling = nextSibling.getWritable(); | ||
|  |         writableNextSibling.__prev = writablePrevSibling.__key; | ||
|  |         writablePrevSibling.__next = writableNextSibling.__key; | ||
|  |       } else { | ||
|  |         writablePrevSibling.__next = null; | ||
|  |       } | ||
|  |       writableNode.__prev = null; | ||
|  |     } | ||
|  |     if (nextSibling === null) { | ||
|  |       if (prevSibling !== null) { | ||
|  |         const writablePrevSibling = prevSibling.getWritable(); | ||
|  |         writableParent.__last = prevSibling.__key; | ||
|  |         writablePrevSibling.__next = null; | ||
|  |       } else { | ||
|  |         writableParent.__last = null; | ||
|  |       } | ||
|  |     } else { | ||
|  |       const writableNextSibling = nextSibling.getWritable(); | ||
|  |       if (prevSibling !== null) { | ||
|  |         const writablePrevSibling = prevSibling.getWritable(); | ||
|  |         writablePrevSibling.__next = writableNextSibling.__key; | ||
|  |         writableNextSibling.__prev = writablePrevSibling.__key; | ||
|  |       } else { | ||
|  |         writableNextSibling.__prev = null; | ||
|  |       } | ||
|  |       writableNode.__next = null; | ||
|  |     } | ||
|  |     writableParent.__size--; | ||
|  |     writableNode.__parent = null; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | // Never use this function directly! It will break
 | ||
|  | // the cloning heuristic. Instead use node.getWritable().
 | ||
|  | export function internalMarkNodeAsDirty(node: LexicalNode): void { | ||
|  |   errorOnInfiniteTransforms(); | ||
|  |   const latest = node.getLatest(); | ||
|  |   const parent = latest.__parent; | ||
|  |   const editorState = getActiveEditorState(); | ||
|  |   const editor = getActiveEditor(); | ||
|  |   const nodeMap = editorState._nodeMap; | ||
|  |   const dirtyElements = editor._dirtyElements; | ||
|  |   if (parent !== null) { | ||
|  |     internalMarkParentElementsAsDirty(parent, nodeMap, dirtyElements); | ||
|  |   } | ||
|  |   const key = latest.__key; | ||
|  |   editor._dirtyType = HAS_DIRTY_NODES; | ||
|  |   if ($isElementNode(node)) { | ||
|  |     dirtyElements.set(key, true); | ||
|  |   } else { | ||
|  |     // TODO split internally MarkNodeAsDirty into two dedicated Element/leave functions
 | ||
|  |     editor._dirtyLeaves.add(key); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function internalMarkSiblingsAsDirty(node: LexicalNode) { | ||
|  |   const previousNode = node.getPreviousSibling(); | ||
|  |   const nextNode = node.getNextSibling(); | ||
|  |   if (previousNode !== null) { | ||
|  |     internalMarkNodeAsDirty(previousNode); | ||
|  |   } | ||
|  |   if (nextNode !== null) { | ||
|  |     internalMarkNodeAsDirty(nextNode); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $setCompositionKey(compositionKey: null | NodeKey): void { | ||
|  |   errorOnReadOnly(); | ||
|  |   const editor = getActiveEditor(); | ||
|  |   const previousCompositionKey = editor._compositionKey; | ||
|  |   if (compositionKey !== previousCompositionKey) { | ||
|  |     editor._compositionKey = compositionKey; | ||
|  |     if (previousCompositionKey !== null) { | ||
|  |       const node = $getNodeByKey(previousCompositionKey); | ||
|  |       if (node !== null) { | ||
|  |         node.getWritable(); | ||
|  |       } | ||
|  |     } | ||
|  |     if (compositionKey !== null) { | ||
|  |       const node = $getNodeByKey(compositionKey); | ||
|  |       if (node !== null) { | ||
|  |         node.getWritable(); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $getCompositionKey(): null | NodeKey { | ||
|  |   if (isCurrentlyReadOnlyMode()) { | ||
|  |     return null; | ||
|  |   } | ||
|  |   const editor = getActiveEditor(); | ||
|  |   return editor._compositionKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNodeByKey<T extends LexicalNode>( | ||
|  |   key: NodeKey, | ||
|  |   _editorState?: EditorState, | ||
|  | ): T | null { | ||
|  |   const editorState = _editorState || getActiveEditorState(); | ||
|  |   const node = editorState._nodeMap.get(key) as T; | ||
|  |   if (node === undefined) { | ||
|  |     return null; | ||
|  |   } | ||
|  |   return node; | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNodeFromDOMNode( | ||
|  |   dom: Node, | ||
|  |   editorState?: EditorState, | ||
|  | ): LexicalNode | null { | ||
|  |   const editor = getActiveEditor(); | ||
|  |   // @ts-ignore We intentionally add this to the Node.
 | ||
|  |   const key = dom[`__lexicalKey_${editor._key}`]; | ||
|  |   if (key !== undefined) { | ||
|  |     return $getNodeByKey(key, editorState); | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNearestNodeFromDOMNode( | ||
|  |   startingDOM: Node, | ||
|  |   editorState?: EditorState, | ||
|  | ): LexicalNode | null { | ||
|  |   let dom: Node | null = startingDOM; | ||
|  |   while (dom != null) { | ||
|  |     const node = $getNodeFromDOMNode(dom, editorState); | ||
|  |     if (node !== null) { | ||
|  |       return node; | ||
|  |     } | ||
|  |     dom = getParentElement(dom); | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function cloneDecorators( | ||
|  |   editor: LexicalEditor, | ||
|  | ): Record<NodeKey, unknown> { | ||
|  |   const currentDecorators = editor._decorators; | ||
|  |   const pendingDecorators = Object.assign({}, currentDecorators); | ||
|  |   editor._pendingDecorators = pendingDecorators; | ||
|  |   return pendingDecorators; | ||
|  | } | ||
|  | 
 | ||
|  | export function getEditorStateTextContent(editorState: EditorState): string { | ||
|  |   return editorState.read(() => $getRoot().getTextContent()); | ||
|  | } | ||
|  | 
 | ||
|  | export function markAllNodesAsDirty(editor: LexicalEditor, type: string): void { | ||
|  |   // Mark all existing text nodes as dirty
 | ||
|  |   updateEditor( | ||
|  |     editor, | ||
|  |     () => { | ||
|  |       const editorState = getActiveEditorState(); | ||
|  |       if (editorState.isEmpty()) { | ||
|  |         return; | ||
|  |       } | ||
|  |       if (type === 'root') { | ||
|  |         $getRoot().markDirty(); | ||
|  |         return; | ||
|  |       } | ||
|  |       const nodeMap = editorState._nodeMap; | ||
|  |       for (const [, node] of nodeMap) { | ||
|  |         node.markDirty(); | ||
|  |       } | ||
|  |     }, | ||
|  |     editor._pendingEditorState === null | ||
|  |       ? { | ||
|  |           tag: 'history-merge', | ||
|  |         } | ||
|  |       : undefined, | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function $getRoot(): RootNode { | ||
|  |   return internalGetRoot(getActiveEditorState()); | ||
|  | } | ||
|  | 
 | ||
|  | export function internalGetRoot(editorState: EditorState): RootNode { | ||
|  |   return editorState._nodeMap.get('root') as RootNode; | ||
|  | } | ||
|  | 
 | ||
|  | export function $setSelection(selection: null | BaseSelection): void { | ||
|  |   errorOnReadOnly(); | ||
|  |   const editorState = getActiveEditorState(); | ||
|  |   if (selection !== null) { | ||
|  |     if (__DEV__) { | ||
|  |       if (Object.isFrozen(selection)) { | ||
|  |         invariant( | ||
|  |           false, | ||
|  |           '$setSelection called on frozen selection object. Ensure selection is cloned before passing in.', | ||
|  |         ); | ||
|  |       } | ||
|  |     } | ||
|  |     selection.dirty = true; | ||
|  |     selection.setCachedNodes(null); | ||
|  |   } | ||
|  |   editorState._selection = selection; | ||
|  | } | ||
|  | 
 | ||
|  | export function $flushMutations(): void { | ||
|  |   errorOnReadOnly(); | ||
|  |   const editor = getActiveEditor(); | ||
|  |   $flushRootMutations(editor); | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNodeFromDOM(dom: Node): null | LexicalNode { | ||
|  |   const editor = getActiveEditor(); | ||
|  |   const nodeKey = getNodeKeyFromDOM(dom, editor); | ||
|  |   if (nodeKey === null) { | ||
|  |     const rootElement = editor.getRootElement(); | ||
|  |     if (dom === rootElement) { | ||
|  |       return $getNodeByKey('root'); | ||
|  |     } | ||
|  |     return null; | ||
|  |   } | ||
|  |   return $getNodeByKey(nodeKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function getTextNodeOffset( | ||
|  |   node: TextNode, | ||
|  |   moveSelectionToEnd: boolean, | ||
|  | ): number { | ||
|  |   return moveSelectionToEnd ? node.getTextContentSize() : 0; | ||
|  | } | ||
|  | 
 | ||
|  | function getNodeKeyFromDOM( | ||
|  |   // Note that node here refers to a DOM Node, not an Lexical Node
 | ||
|  |   dom: Node, | ||
|  |   editor: LexicalEditor, | ||
|  | ): NodeKey | null { | ||
|  |   let node: Node | null = dom; | ||
|  |   while (node != null) { | ||
|  |     // @ts-ignore We intentionally add this to the Node.
 | ||
|  |     const key: NodeKey = node[`__lexicalKey_${editor._key}`]; | ||
|  |     if (key !== undefined) { | ||
|  |       return key; | ||
|  |     } | ||
|  |     node = getParentElement(node); | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function doesContainGrapheme(str: string): boolean { | ||
|  |   return /[\uD800-\uDBFF][\uDC00-\uDFFF]/g.test(str); | ||
|  | } | ||
|  | 
 | ||
|  | export function getEditorsToPropagate( | ||
|  |   editor: LexicalEditor, | ||
|  | ): Array<LexicalEditor> { | ||
|  |   const editorsToPropagate = []; | ||
|  |   let currentEditor: LexicalEditor | null = editor; | ||
|  |   while (currentEditor !== null) { | ||
|  |     editorsToPropagate.push(currentEditor); | ||
|  |     currentEditor = currentEditor._parentEditor; | ||
|  |   } | ||
|  |   return editorsToPropagate; | ||
|  | } | ||
|  | 
 | ||
|  | export function createUID(): string { | ||
|  |   return Math.random() | ||
|  |     .toString(36) | ||
|  |     .replace(/[^a-z]+/g, '') | ||
|  |     .substr(0, 5); | ||
|  | } | ||
|  | 
 | ||
|  | export function getAnchorTextFromDOM(anchorNode: Node): null | string { | ||
|  |   if (anchorNode.nodeType === DOM_TEXT_TYPE) { | ||
|  |     return anchorNode.nodeValue; | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function $updateSelectedTextFromDOM( | ||
|  |   isCompositionEnd: boolean, | ||
|  |   editor: LexicalEditor, | ||
|  |   data?: string, | ||
|  | ): void { | ||
|  |   // Update the text content with the latest composition text
 | ||
|  |   const domSelection = getDOMSelection(editor._window); | ||
|  |   if (domSelection === null) { | ||
|  |     return; | ||
|  |   } | ||
|  |   const anchorNode = domSelection.anchorNode; | ||
|  |   let {anchorOffset, focusOffset} = domSelection; | ||
|  |   if (anchorNode !== null) { | ||
|  |     let textContent = getAnchorTextFromDOM(anchorNode); | ||
|  |     const node = $getNearestNodeFromDOMNode(anchorNode); | ||
|  |     if (textContent !== null && $isTextNode(node)) { | ||
|  |       // Data is intentionally truthy, as we check for boolean, null and empty string.
 | ||
|  |       if (textContent === COMPOSITION_SUFFIX && data) { | ||
|  |         const offset = data.length; | ||
|  |         textContent = data; | ||
|  |         anchorOffset = offset; | ||
|  |         focusOffset = offset; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (textContent !== null) { | ||
|  |         $updateTextNodeFromDOMContent( | ||
|  |           node, | ||
|  |           textContent, | ||
|  |           anchorOffset, | ||
|  |           focusOffset, | ||
|  |           isCompositionEnd, | ||
|  |         ); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $updateTextNodeFromDOMContent( | ||
|  |   textNode: TextNode, | ||
|  |   textContent: string, | ||
|  |   anchorOffset: null | number, | ||
|  |   focusOffset: null | number, | ||
|  |   compositionEnd: boolean, | ||
|  | ): void { | ||
|  |   let node = textNode; | ||
|  | 
 | ||
|  |   if (node.isAttached() && (compositionEnd || !node.isDirty())) { | ||
|  |     const isComposing = node.isComposing(); | ||
|  |     let normalizedTextContent = textContent; | ||
|  | 
 | ||
|  |     if ( | ||
|  |       (isComposing || compositionEnd) && | ||
|  |       textContent[textContent.length - 1] === COMPOSITION_SUFFIX | ||
|  |     ) { | ||
|  |       normalizedTextContent = textContent.slice(0, -1); | ||
|  |     } | ||
|  |     const prevTextContent = node.getTextContent(); | ||
|  | 
 | ||
|  |     if (compositionEnd || normalizedTextContent !== prevTextContent) { | ||
|  |       if (normalizedTextContent === '') { | ||
|  |         $setCompositionKey(null); | ||
|  |         if (!IS_SAFARI && !IS_IOS && !IS_APPLE_WEBKIT) { | ||
|  |           // For composition (mainly Android), we have to remove the node on a later update
 | ||
|  |           const editor = getActiveEditor(); | ||
|  |           setTimeout(() => { | ||
|  |             editor.update(() => { | ||
|  |               if (node.isAttached()) { | ||
|  |                 node.remove(); | ||
|  |               } | ||
|  |             }); | ||
|  |           }, 20); | ||
|  |         } else { | ||
|  |           node.remove(); | ||
|  |         } | ||
|  |         return; | ||
|  |       } | ||
|  |       const parent = node.getParent(); | ||
|  |       const prevSelection = $getPreviousSelection(); | ||
|  |       const prevTextContentSize = node.getTextContentSize(); | ||
|  |       const compositionKey = $getCompositionKey(); | ||
|  |       const nodeKey = node.getKey(); | ||
|  | 
 | ||
|  |       if ( | ||
|  |         node.isToken() || | ||
|  |         (compositionKey !== null && | ||
|  |           nodeKey === compositionKey && | ||
|  |           !isComposing) || | ||
|  |         // Check if character was added at the start or boundaries when not insertable, and we need
 | ||
|  |         // to clear this input from occurring as that action wasn't permitted.
 | ||
|  |         ($isRangeSelection(prevSelection) && | ||
|  |           ((parent !== null && | ||
|  |             !parent.canInsertTextBefore() && | ||
|  |             prevSelection.anchor.offset === 0) || | ||
|  |             (prevSelection.anchor.key === textNode.__key && | ||
|  |               prevSelection.anchor.offset === 0 && | ||
|  |               !node.canInsertTextBefore() && | ||
|  |               !isComposing) || | ||
|  |             (prevSelection.focus.key === textNode.__key && | ||
|  |               prevSelection.focus.offset === prevTextContentSize && | ||
|  |               !node.canInsertTextAfter() && | ||
|  |               !isComposing))) | ||
|  |       ) { | ||
|  |         node.markDirty(); | ||
|  |         return; | ||
|  |       } | ||
|  |       const selection = $getSelection(); | ||
|  | 
 | ||
|  |       if ( | ||
|  |         !$isRangeSelection(selection) || | ||
|  |         anchorOffset === null || | ||
|  |         focusOffset === null | ||
|  |       ) { | ||
|  |         node.setTextContent(normalizedTextContent); | ||
|  |         return; | ||
|  |       } | ||
|  |       selection.setTextNodeRange(node, anchorOffset, node, focusOffset); | ||
|  | 
 | ||
|  |       if (node.isSegmented()) { | ||
|  |         const originalTextContent = node.getTextContent(); | ||
|  |         const replacement = $createTextNode(originalTextContent); | ||
|  |         node.replace(replacement); | ||
|  |         node = replacement; | ||
|  |       } | ||
|  |       node.setTextContent(normalizedTextContent); | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function $previousSiblingDoesNotAcceptText(node: TextNode): boolean { | ||
|  |   const previousSibling = node.getPreviousSibling(); | ||
|  | 
 | ||
|  |   return ( | ||
|  |     ($isTextNode(previousSibling) || | ||
|  |       ($isElementNode(previousSibling) && previousSibling.isInline())) && | ||
|  |     !previousSibling.canInsertTextAfter() | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | // This function is connected to $shouldPreventDefaultAndInsertText and determines whether the
 | ||
|  | // TextNode boundaries are writable or we should use the previous/next sibling instead. For example,
 | ||
|  | // in the case of a LinkNode, boundaries are not writable.
 | ||
|  | export function $shouldInsertTextAfterOrBeforeTextNode( | ||
|  |   selection: RangeSelection, | ||
|  |   node: TextNode, | ||
|  | ): boolean { | ||
|  |   if (node.isSegmented()) { | ||
|  |     return true; | ||
|  |   } | ||
|  |   if (!selection.isCollapsed()) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   const offset = selection.anchor.offset; | ||
|  |   const parent = node.getParentOrThrow(); | ||
|  |   const isToken = node.isToken(); | ||
|  |   if (offset === 0) { | ||
|  |     return ( | ||
|  |       !node.canInsertTextBefore() || | ||
|  |       (!parent.canInsertTextBefore() && !node.isComposing()) || | ||
|  |       isToken || | ||
|  |       $previousSiblingDoesNotAcceptText(node) | ||
|  |     ); | ||
|  |   } else if (offset === node.getTextContentSize()) { | ||
|  |     return ( | ||
|  |       !node.canInsertTextAfter() || | ||
|  |       (!parent.canInsertTextAfter() && !node.isComposing()) || | ||
|  |       isToken | ||
|  |     ); | ||
|  |   } else { | ||
|  |     return false; | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function isTab( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return key === 'Tab' && !altKey && !ctrlKey && !metaKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isBold( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return ( | ||
|  |     key.toLowerCase() === 'b' && !altKey && controlOrMeta(metaKey, ctrlKey) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isItalic( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return ( | ||
|  |     key.toLowerCase() === 'i' && !altKey && controlOrMeta(metaKey, ctrlKey) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isUnderline( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return ( | ||
|  |     key.toLowerCase() === 'u' && !altKey && controlOrMeta(metaKey, ctrlKey) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isParagraph(key: string, shiftKey: boolean): boolean { | ||
|  |   return isReturn(key) && !shiftKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isLineBreak(key: string, shiftKey: boolean): boolean { | ||
|  |   return isReturn(key) && shiftKey; | ||
|  | } | ||
|  | 
 | ||
|  | // Inserts a new line after the selection
 | ||
|  | 
 | ||
|  | export function isOpenLineBreak(key: string, ctrlKey: boolean): boolean { | ||
|  |   // 79 = KeyO
 | ||
|  |   return IS_APPLE && ctrlKey && key.toLowerCase() === 'o'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteWordBackward( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isBackspace(key) && (IS_APPLE ? altKey : ctrlKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteWordForward( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isDelete(key) && (IS_APPLE ? altKey : ctrlKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteLineBackward(key: string, metaKey: boolean): boolean { | ||
|  |   return IS_APPLE && metaKey && isBackspace(key); | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteLineForward(key: string, metaKey: boolean): boolean { | ||
|  |   return IS_APPLE && metaKey && isDelete(key); | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteBackward( | ||
|  |   key: string, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   if (IS_APPLE) { | ||
|  |     if (altKey || metaKey) { | ||
|  |       return false; | ||
|  |     } | ||
|  |     return isBackspace(key) || (key.toLowerCase() === 'h' && ctrlKey); | ||
|  |   } | ||
|  |   if (ctrlKey || altKey || metaKey) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   return isBackspace(key); | ||
|  | } | ||
|  | 
 | ||
|  | export function isDeleteForward( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   shiftKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   if (IS_APPLE) { | ||
|  |     if (shiftKey || altKey || metaKey) { | ||
|  |       return false; | ||
|  |     } | ||
|  |     return isDelete(key) || (key.toLowerCase() === 'd' && ctrlKey); | ||
|  |   } | ||
|  |   if (ctrlKey || altKey || metaKey) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   return isDelete(key); | ||
|  | } | ||
|  | 
 | ||
|  | export function isUndo( | ||
|  |   key: string, | ||
|  |   shiftKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return ( | ||
|  |     key.toLowerCase() === 'z' && !shiftKey && controlOrMeta(metaKey, ctrlKey) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isRedo( | ||
|  |   key: string, | ||
|  |   shiftKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   if (IS_APPLE) { | ||
|  |     return key.toLowerCase() === 'z' && metaKey && shiftKey; | ||
|  |   } | ||
|  |   return ( | ||
|  |     (key.toLowerCase() === 'y' && ctrlKey) || | ||
|  |     (key.toLowerCase() === 'z' && ctrlKey && shiftKey) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function isCopy( | ||
|  |   key: string, | ||
|  |   shiftKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   if (shiftKey) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   if (key.toLowerCase() === 'c') { | ||
|  |     return IS_APPLE ? metaKey : ctrlKey; | ||
|  |   } | ||
|  | 
 | ||
|  |   return false; | ||
|  | } | ||
|  | 
 | ||
|  | export function isCut( | ||
|  |   key: string, | ||
|  |   shiftKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   if (shiftKey) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   if (key.toLowerCase() === 'x') { | ||
|  |     return IS_APPLE ? metaKey : ctrlKey; | ||
|  |   } | ||
|  | 
 | ||
|  |   return false; | ||
|  | } | ||
|  | 
 | ||
|  | function isArrowLeft(key: string): boolean { | ||
|  |   return key === 'ArrowLeft'; | ||
|  | } | ||
|  | 
 | ||
|  | function isArrowRight(key: string): boolean { | ||
|  |   return key === 'ArrowRight'; | ||
|  | } | ||
|  | 
 | ||
|  | function isArrowUp(key: string): boolean { | ||
|  |   return key === 'ArrowUp'; | ||
|  | } | ||
|  | 
 | ||
|  | function isArrowDown(key: string): boolean { | ||
|  |   return key === 'ArrowDown'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveBackward( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowLeft(key) && !ctrlKey && !metaKey && !altKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveToStart( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   shiftKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowLeft(key) && !altKey && !shiftKey && (ctrlKey || metaKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveForward( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowRight(key) && !ctrlKey && !metaKey && !altKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveToEnd( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   shiftKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowRight(key) && !altKey && !shiftKey && (ctrlKey || metaKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveUp( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowUp(key) && !ctrlKey && !metaKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isMoveDown( | ||
|  |   key: string, | ||
|  |   ctrlKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return isArrowDown(key) && !ctrlKey && !metaKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isModifier( | ||
|  |   ctrlKey: boolean, | ||
|  |   shiftKey: boolean, | ||
|  |   altKey: boolean, | ||
|  |   metaKey: boolean, | ||
|  | ): boolean { | ||
|  |   return ctrlKey || shiftKey || altKey || metaKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isSpace(key: string): boolean { | ||
|  |   return key === ' '; | ||
|  | } | ||
|  | 
 | ||
|  | export function controlOrMeta(metaKey: boolean, ctrlKey: boolean): boolean { | ||
|  |   if (IS_APPLE) { | ||
|  |     return metaKey; | ||
|  |   } | ||
|  |   return ctrlKey; | ||
|  | } | ||
|  | 
 | ||
|  | export function isReturn(key: string): boolean { | ||
|  |   return key === 'Enter'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isBackspace(key: string): boolean { | ||
|  |   return key === 'Backspace'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isEscape(key: string): boolean { | ||
|  |   return key === 'Escape'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isDelete(key: string): boolean { | ||
|  |   return key === 'Delete'; | ||
|  | } | ||
|  | 
 | ||
|  | export function isSelectAll( | ||
|  |   key: string, | ||
|  |   metaKey: boolean, | ||
|  |   ctrlKey: boolean, | ||
|  | ): boolean { | ||
|  |   return key.toLowerCase() === 'a' && controlOrMeta(metaKey, ctrlKey); | ||
|  | } | ||
|  | 
 | ||
|  | export function $selectAll(): void { | ||
|  |   const root = $getRoot(); | ||
|  |   const selection = root.select(0, root.getChildrenSize()); | ||
|  |   $setSelection($normalizeSelection(selection)); | ||
|  | } | ||
|  | 
 | ||
|  | export function getCachedClassNameArray( | ||
|  |   classNamesTheme: EditorThemeClasses, | ||
|  |   classNameThemeType: string, | ||
|  | ): Array<string> { | ||
|  |   if (classNamesTheme.__lexicalClassNameCache === undefined) { | ||
|  |     classNamesTheme.__lexicalClassNameCache = {}; | ||
|  |   } | ||
|  |   const classNamesCache = classNamesTheme.__lexicalClassNameCache; | ||
|  |   const cachedClassNames = classNamesCache[classNameThemeType]; | ||
|  |   if (cachedClassNames !== undefined) { | ||
|  |     return cachedClassNames; | ||
|  |   } | ||
|  |   const classNames = classNamesTheme[classNameThemeType]; | ||
|  |   // As we're using classList, we need
 | ||
|  |   // to handle className tokens that have spaces.
 | ||
|  |   // The easiest way to do this to convert the
 | ||
|  |   // className tokens to an array that can be
 | ||
|  |   // applied to classList.add()/remove().
 | ||
|  |   if (typeof classNames === 'string') { | ||
|  |     const classNamesArr = normalizeClassNames(classNames); | ||
|  |     classNamesCache[classNameThemeType] = classNamesArr; | ||
|  |     return classNamesArr; | ||
|  |   } | ||
|  |   return classNames; | ||
|  | } | ||
|  | 
 | ||
|  | export function setMutatedNode( | ||
|  |   mutatedNodes: MutatedNodes, | ||
|  |   registeredNodes: RegisteredNodes, | ||
|  |   mutationListeners: MutationListeners, | ||
|  |   node: LexicalNode, | ||
|  |   mutation: NodeMutation, | ||
|  | ) { | ||
|  |   if (mutationListeners.size === 0) { | ||
|  |     return; | ||
|  |   } | ||
|  |   const nodeType = node.__type; | ||
|  |   const nodeKey = node.__key; | ||
|  |   const registeredNode = registeredNodes.get(nodeType); | ||
|  |   if (registeredNode === undefined) { | ||
|  |     invariant(false, 'Type %s not in registeredNodes', nodeType); | ||
|  |   } | ||
|  |   const klass = registeredNode.klass; | ||
|  |   let mutatedNodesByType = mutatedNodes.get(klass); | ||
|  |   if (mutatedNodesByType === undefined) { | ||
|  |     mutatedNodesByType = new Map(); | ||
|  |     mutatedNodes.set(klass, mutatedNodesByType); | ||
|  |   } | ||
|  |   const prevMutation = mutatedNodesByType.get(nodeKey); | ||
|  |   // If the node has already been "destroyed", yet we are
 | ||
|  |   // re-making it, then this means a move likely happened.
 | ||
|  |   // We should change the mutation to be that of "updated"
 | ||
|  |   // instead.
 | ||
|  |   const isMove = prevMutation === 'destroyed' && mutation === 'created'; | ||
|  |   if (prevMutation === undefined || isMove) { | ||
|  |     mutatedNodesByType.set(nodeKey, isMove ? 'updated' : mutation); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $nodesOfType<T extends LexicalNode>(klass: Klass<T>): Array<T> { | ||
|  |   const klassType = klass.getType(); | ||
|  |   const editorState = getActiveEditorState(); | ||
|  |   if (editorState._readOnly) { | ||
|  |     const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as | ||
|  |       | undefined | ||
|  |       | Map<string, T>; | ||
|  |     return nodes ? Array.from(nodes.values()) : []; | ||
|  |   } | ||
|  |   const nodes = editorState._nodeMap; | ||
|  |   const nodesOfType: Array<T> = []; | ||
|  |   for (const [, node] of nodes) { | ||
|  |     if ( | ||
|  |       node instanceof klass && | ||
|  |       node.__type === klassType && | ||
|  |       node.isAttached() | ||
|  |     ) { | ||
|  |       nodesOfType.push(node as T); | ||
|  |     } | ||
|  |   } | ||
|  |   return nodesOfType; | ||
|  | } | ||
|  | 
 | ||
|  | function resolveElement( | ||
|  |   element: ElementNode, | ||
|  |   isBackward: boolean, | ||
|  |   focusOffset: number, | ||
|  | ): LexicalNode | null { | ||
|  |   const parent = element.getParent(); | ||
|  |   let offset = focusOffset; | ||
|  |   let block = element; | ||
|  |   if (parent !== null) { | ||
|  |     if (isBackward && focusOffset === 0) { | ||
|  |       offset = block.getIndexWithinParent(); | ||
|  |       block = parent; | ||
|  |     } else if (!isBackward && focusOffset === block.getChildrenSize()) { | ||
|  |       offset = block.getIndexWithinParent() + 1; | ||
|  |       block = parent; | ||
|  |     } | ||
|  |   } | ||
|  |   return block.getChildAtIndex(isBackward ? offset - 1 : offset); | ||
|  | } | ||
|  | 
 | ||
|  | export function $getAdjacentNode( | ||
|  |   focus: PointType, | ||
|  |   isBackward: boolean, | ||
|  | ): null | LexicalNode { | ||
|  |   const focusOffset = focus.offset; | ||
|  |   if (focus.type === 'element') { | ||
|  |     const block = focus.getNode(); | ||
|  |     return resolveElement(block, isBackward, focusOffset); | ||
|  |   } else { | ||
|  |     const focusNode = focus.getNode(); | ||
|  |     if ( | ||
|  |       (isBackward && focusOffset === 0) || | ||
|  |       (!isBackward && focusOffset === focusNode.getTextContentSize()) | ||
|  |     ) { | ||
|  |       const possibleNode = isBackward | ||
|  |         ? focusNode.getPreviousSibling() | ||
|  |         : focusNode.getNextSibling(); | ||
|  |       if (possibleNode === null) { | ||
|  |         return resolveElement( | ||
|  |           focusNode.getParentOrThrow(), | ||
|  |           isBackward, | ||
|  |           focusNode.getIndexWithinParent() + (isBackward ? 0 : 1), | ||
|  |         ); | ||
|  |       } | ||
|  |       return possibleNode; | ||
|  |     } | ||
|  |   } | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | export function isFirefoxClipboardEvents(editor: LexicalEditor): boolean { | ||
|  |   const event = getWindow(editor).event; | ||
|  |   const inputType = event && (event as InputEvent).inputType; | ||
|  |   return ( | ||
|  |     inputType === 'insertFromPaste' || | ||
|  |     inputType === 'insertFromPasteAsQuotation' | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function dispatchCommand<TCommand extends LexicalCommand<unknown>>( | ||
|  |   editor: LexicalEditor, | ||
|  |   command: TCommand, | ||
|  |   payload: CommandPayloadType<TCommand>, | ||
|  | ): boolean { | ||
|  |   return triggerCommandListeners(editor, command, payload); | ||
|  | } | ||
|  | 
 | ||
|  | export function $textContentRequiresDoubleLinebreakAtEnd( | ||
|  |   node: ElementNode, | ||
|  | ): boolean { | ||
|  |   return !$isRootNode(node) && !node.isLastChild() && !node.isInline(); | ||
|  | } | ||
|  | 
 | ||
|  | export function getElementByKeyOrThrow( | ||
|  |   editor: LexicalEditor, | ||
|  |   key: NodeKey, | ||
|  | ): HTMLElement { | ||
|  |   const element = editor._keyToDOMMap.get(key); | ||
|  | 
 | ||
|  |   if (element === undefined) { | ||
|  |     invariant( | ||
|  |       false, | ||
|  |       'Reconciliation: could not find DOM element for node key %s', | ||
|  |       key, | ||
|  |     ); | ||
|  |   } | ||
|  | 
 | ||
|  |   return element; | ||
|  | } | ||
|  | 
 | ||
|  | export function getParentElement(node: Node): HTMLElement | null { | ||
|  |   const parentElement = | ||
|  |     (node as HTMLSlotElement).assignedSlot || node.parentElement; | ||
|  |   return parentElement !== null && parentElement.nodeType === 11 | ||
|  |     ? ((parentElement as unknown as ShadowRoot).host as HTMLElement) | ||
|  |     : parentElement; | ||
|  | } | ||
|  | 
 | ||
|  | export function scrollIntoViewIfNeeded( | ||
|  |   editor: LexicalEditor, | ||
|  |   selectionRect: DOMRect, | ||
|  |   rootElement: HTMLElement, | ||
|  | ): void { | ||
|  |   const doc = rootElement.ownerDocument; | ||
|  |   const defaultView = doc.defaultView; | ||
|  | 
 | ||
|  |   if (defaultView === null) { | ||
|  |     return; | ||
|  |   } | ||
|  |   let {top: currentTop, bottom: currentBottom} = selectionRect; | ||
|  |   let targetTop = 0; | ||
|  |   let targetBottom = 0; | ||
|  |   let element: HTMLElement | null = rootElement; | ||
|  | 
 | ||
|  |   while (element !== null) { | ||
|  |     const isBodyElement = element === doc.body; | ||
|  |     if (isBodyElement) { | ||
|  |       targetTop = 0; | ||
|  |       targetBottom = getWindow(editor).innerHeight; | ||
|  |     } else { | ||
|  |       const targetRect = element.getBoundingClientRect(); | ||
|  |       targetTop = targetRect.top; | ||
|  |       targetBottom = targetRect.bottom; | ||
|  |     } | ||
|  |     let diff = 0; | ||
|  | 
 | ||
|  |     if (currentTop < targetTop) { | ||
|  |       diff = -(targetTop - currentTop); | ||
|  |     } else if (currentBottom > targetBottom) { | ||
|  |       diff = currentBottom - targetBottom; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (diff !== 0) { | ||
|  |       if (isBodyElement) { | ||
|  |         // Only handles scrolling of Y axis
 | ||
|  |         defaultView.scrollBy(0, diff); | ||
|  |       } else { | ||
|  |         const scrollTop = element.scrollTop; | ||
|  |         element.scrollTop += diff; | ||
|  |         const yOffset = element.scrollTop - scrollTop; | ||
|  |         currentTop -= yOffset; | ||
|  |         currentBottom -= yOffset; | ||
|  |       } | ||
|  |     } | ||
|  |     if (isBodyElement) { | ||
|  |       break; | ||
|  |     } | ||
|  |     element = getParentElement(element); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $hasUpdateTag(tag: string): boolean { | ||
|  |   const editor = getActiveEditor(); | ||
|  |   return editor._updateTags.has(tag); | ||
|  | } | ||
|  | 
 | ||
|  | export function $addUpdateTag(tag: string): void { | ||
|  |   errorOnReadOnly(); | ||
|  |   const editor = getActiveEditor(); | ||
|  |   editor._updateTags.add(tag); | ||
|  | } | ||
|  | 
 | ||
|  | export function $maybeMoveChildrenSelectionToParent( | ||
|  |   parentNode: LexicalNode, | ||
|  | ): BaseSelection | null { | ||
|  |   const selection = $getSelection(); | ||
|  |   if (!$isRangeSelection(selection) || !$isElementNode(parentNode)) { | ||
|  |     return selection; | ||
|  |   } | ||
|  |   const {anchor, focus} = selection; | ||
|  |   const anchorNode = anchor.getNode(); | ||
|  |   const focusNode = focus.getNode(); | ||
|  |   if ($hasAncestor(anchorNode, parentNode)) { | ||
|  |     anchor.set(parentNode.__key, 0, 'element'); | ||
|  |   } | ||
|  |   if ($hasAncestor(focusNode, parentNode)) { | ||
|  |     focus.set(parentNode.__key, 0, 'element'); | ||
|  |   } | ||
|  |   return selection; | ||
|  | } | ||
|  | 
 | ||
|  | export function $hasAncestor( | ||
|  |   child: LexicalNode, | ||
|  |   targetNode: LexicalNode, | ||
|  | ): boolean { | ||
|  |   let parent = child.getParent(); | ||
|  |   while (parent !== null) { | ||
|  |     if (parent.is(targetNode)) { | ||
|  |       return true; | ||
|  |     } | ||
|  |     parent = parent.getParent(); | ||
|  |   } | ||
|  |   return false; | ||
|  | } | ||
|  | 
 | ||
|  | export function getDefaultView(domElem: HTMLElement): Window | null { | ||
|  |   const ownerDoc = domElem.ownerDocument; | ||
|  |   return (ownerDoc && ownerDoc.defaultView) || null; | ||
|  | } | ||
|  | 
 | ||
|  | export function getWindow(editor: LexicalEditor): Window { | ||
|  |   const windowObj = editor._window; | ||
|  |   if (windowObj === null) { | ||
|  |     invariant(false, 'window object not found'); | ||
|  |   } | ||
|  |   return windowObj; | ||
|  | } | ||
|  | 
 | ||
|  | export function $isInlineElementOrDecoratorNode(node: LexicalNode): boolean { | ||
|  |   return ( | ||
|  |     ($isElementNode(node) && node.isInline()) || | ||
|  |     ($isDecoratorNode(node) && node.isInline()) | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNearestRootOrShadowRoot( | ||
|  |   node: LexicalNode, | ||
|  | ): RootNode | ElementNode { | ||
|  |   let parent = node.getParentOrThrow(); | ||
|  |   while (parent !== null) { | ||
|  |     if ($isRootOrShadowRoot(parent)) { | ||
|  |       return parent; | ||
|  |     } | ||
|  |     parent = parent.getParentOrThrow(); | ||
|  |   } | ||
|  |   return parent; | ||
|  | } | ||
|  | 
 | ||
|  | const ShadowRootNodeBrand: unique symbol = Symbol.for( | ||
|  |   '@lexical/ShadowRootNodeBrand', | ||
|  | ); | ||
|  | type ShadowRootNode = Spread< | ||
|  |   {isShadowRoot(): true; [ShadowRootNodeBrand]: never}, | ||
|  |   ElementNode | ||
|  | >; | ||
|  | export function $isRootOrShadowRoot( | ||
|  |   node: null | LexicalNode, | ||
|  | ): node is RootNode | ShadowRootNode { | ||
|  |   return $isRootNode(node) || ($isElementNode(node) && node.isShadowRoot()); | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Returns a shallow clone of node with a new key | ||
|  |  * | ||
|  |  * @param node - The node to be copied. | ||
|  |  * @returns The copy of the node. | ||
|  |  */ | ||
|  | export function $copyNode<T extends LexicalNode>(node: T): T { | ||
|  |   const copy = node.constructor.clone(node) as T; | ||
|  |   $setNodeKey(copy, null); | ||
|  |   return copy; | ||
|  | } | ||
|  | 
 | ||
|  | export function $applyNodeReplacement<N extends LexicalNode>( | ||
|  |   node: LexicalNode, | ||
|  | ): N { | ||
|  |   const editor = getActiveEditor(); | ||
|  |   const nodeType = node.constructor.getType(); | ||
|  |   const registeredNode = editor._nodes.get(nodeType); | ||
|  |   if (registeredNode === undefined) { | ||
|  |     invariant( | ||
|  |       false, | ||
|  |       '$initializeNode failed. Ensure node has been registered to the editor. You can do this by passing the node class via the "nodes" array in the editor config.', | ||
|  |     ); | ||
|  |   } | ||
|  |   const replaceFunc = registeredNode.replace; | ||
|  |   if (replaceFunc !== null) { | ||
|  |     const replacementNode = replaceFunc(node) as N; | ||
|  |     if (!(replacementNode instanceof node.constructor)) { | ||
|  |       invariant( | ||
|  |         false, | ||
|  |         '$initializeNode failed. Ensure replacement node is a subclass of the original node.', | ||
|  |       ); | ||
|  |     } | ||
|  |     return replacementNode; | ||
|  |   } | ||
|  |   return node as N; | ||
|  | } | ||
|  | 
 | ||
|  | export function errorOnInsertTextNodeOnRoot( | ||
|  |   node: LexicalNode, | ||
|  |   insertNode: LexicalNode, | ||
|  | ): void { | ||
|  |   const parentNode = node.getParent(); | ||
|  |   if ( | ||
|  |     $isRootNode(parentNode) && | ||
|  |     !$isElementNode(insertNode) && | ||
|  |     !$isDecoratorNode(insertNode) | ||
|  |   ) { | ||
|  |     invariant( | ||
|  |       false, | ||
|  |       'Only element or decorator nodes can be inserted in to the root node', | ||
|  |     ); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function $getNodeByKeyOrThrow<N extends LexicalNode>(key: NodeKey): N { | ||
|  |   const node = $getNodeByKey<N>(key); | ||
|  |   if (node === null) { | ||
|  |     invariant( | ||
|  |       false, | ||
|  |       "Expected node with key %s to exist but it's not in the nodeMap.", | ||
|  |       key, | ||
|  |     ); | ||
|  |   } | ||
|  |   return node; | ||
|  | } | ||
|  | 
 | ||
|  | function createBlockCursorElement(editorConfig: EditorConfig): HTMLDivElement { | ||
|  |   const theme = editorConfig.theme; | ||
|  |   const element = document.createElement('div'); | ||
|  |   element.contentEditable = 'false'; | ||
|  |   element.setAttribute('data-lexical-cursor', 'true'); | ||
|  |   let blockCursorTheme = theme.blockCursor; | ||
|  |   if (blockCursorTheme !== undefined) { | ||
|  |     if (typeof blockCursorTheme === 'string') { | ||
|  |       const classNamesArr = normalizeClassNames(blockCursorTheme); | ||
|  |       // @ts-expect-error: intentional
 | ||
|  |       blockCursorTheme = theme.blockCursor = classNamesArr; | ||
|  |     } | ||
|  |     if (blockCursorTheme !== undefined) { | ||
|  |       element.classList.add(...blockCursorTheme); | ||
|  |     } | ||
|  |   } | ||
|  |   return element; | ||
|  | } | ||
|  | 
 | ||
|  | function needsBlockCursor(node: null | LexicalNode): boolean { | ||
|  |   return ( | ||
|  |     ($isDecoratorNode(node) || ($isElementNode(node) && !node.canBeEmpty())) && | ||
|  |     !node.isInline() | ||
|  |   ); | ||
|  | } | ||
|  | 
 | ||
|  | export function removeDOMBlockCursorElement( | ||
|  |   blockCursorElement: HTMLElement, | ||
|  |   editor: LexicalEditor, | ||
|  |   rootElement: HTMLElement, | ||
|  | ) { | ||
|  |   rootElement.style.removeProperty('caret-color'); | ||
|  |   editor._blockCursorElement = null; | ||
|  |   const parentElement = blockCursorElement.parentElement; | ||
|  |   if (parentElement !== null) { | ||
|  |     parentElement.removeChild(blockCursorElement); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function updateDOMBlockCursorElement( | ||
|  |   editor: LexicalEditor, | ||
|  |   rootElement: HTMLElement, | ||
|  |   nextSelection: null | BaseSelection, | ||
|  | ): void { | ||
|  |   let blockCursorElement = editor._blockCursorElement; | ||
|  | 
 | ||
|  |   if ( | ||
|  |     $isRangeSelection(nextSelection) && | ||
|  |     nextSelection.isCollapsed() && | ||
|  |     nextSelection.anchor.type === 'element' && | ||
|  |     rootElement.contains(document.activeElement) | ||
|  |   ) { | ||
|  |     const anchor = nextSelection.anchor; | ||
|  |     const elementNode = anchor.getNode(); | ||
|  |     const offset = anchor.offset; | ||
|  |     const elementNodeSize = elementNode.getChildrenSize(); | ||
|  |     let isBlockCursor = false; | ||
|  |     let insertBeforeElement: null | HTMLElement = null; | ||
|  | 
 | ||
|  |     if (offset === elementNodeSize) { | ||
|  |       const child = elementNode.getChildAtIndex(offset - 1); | ||
|  |       if (needsBlockCursor(child)) { | ||
|  |         isBlockCursor = true; | ||
|  |       } | ||
|  |     } else { | ||
|  |       const child = elementNode.getChildAtIndex(offset); | ||
|  |       if (needsBlockCursor(child)) { | ||
|  |         const sibling = (child as LexicalNode).getPreviousSibling(); | ||
|  |         if (sibling === null || needsBlockCursor(sibling)) { | ||
|  |           isBlockCursor = true; | ||
|  |           insertBeforeElement = editor.getElementByKey( | ||
|  |             (child as LexicalNode).__key, | ||
|  |           ); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |     if (isBlockCursor) { | ||
|  |       const elementDOM = editor.getElementByKey( | ||
|  |         elementNode.__key, | ||
|  |       ) as HTMLElement; | ||
|  |       if (blockCursorElement === null) { | ||
|  |         editor._blockCursorElement = blockCursorElement = | ||
|  |           createBlockCursorElement(editor._config); | ||
|  |       } | ||
|  |       rootElement.style.caretColor = 'transparent'; | ||
|  |       if (insertBeforeElement === null) { | ||
|  |         elementDOM.appendChild(blockCursorElement); | ||
|  |       } else { | ||
|  |         elementDOM.insertBefore(blockCursorElement, insertBeforeElement); | ||
|  |       } | ||
|  |       return; | ||
|  |     } | ||
|  |   } | ||
|  |   // Remove cursor
 | ||
|  |   if (blockCursorElement !== null) { | ||
|  |     removeDOMBlockCursorElement(blockCursorElement, editor, rootElement); | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | export function getDOMSelection(targetWindow: null | Window): null | Selection { | ||
|  |   return !CAN_USE_DOM ? null : (targetWindow || window).getSelection(); | ||
|  | } | ||
|  | 
 | ||
|  | export function $splitNode( | ||
|  |   node: ElementNode, | ||
|  |   offset: number, | ||
|  | ): [ElementNode | null, ElementNode] { | ||
|  |   let startNode = node.getChildAtIndex(offset); | ||
|  |   if (startNode == null) { | ||
|  |     startNode = node; | ||
|  |   } | ||
|  | 
 | ||
|  |   invariant( | ||
|  |     !$isRootOrShadowRoot(node), | ||
|  |     'Can not call $splitNode() on root element', | ||
|  |   ); | ||
|  | 
 | ||
|  |   const recurse = <T extends LexicalNode>( | ||
|  |     currentNode: T, | ||
|  |   ): [ElementNode, ElementNode, T] => { | ||
|  |     const parent = currentNode.getParentOrThrow(); | ||
|  |     const isParentRoot = $isRootOrShadowRoot(parent); | ||
|  |     // The node we start split from (leaf) is moved, but its recursive
 | ||
|  |     // parents are copied to create separate tree
 | ||
|  |     const nodeToMove = | ||
|  |       currentNode === startNode && !isParentRoot | ||
|  |         ? currentNode | ||
|  |         : $copyNode(currentNode); | ||
|  | 
 | ||
|  |     if (isParentRoot) { | ||
|  |       invariant( | ||
|  |         $isElementNode(currentNode) && $isElementNode(nodeToMove), | ||
|  |         'Children of a root must be ElementNode', | ||
|  |       ); | ||
|  | 
 | ||
|  |       currentNode.insertAfter(nodeToMove); | ||
|  |       return [currentNode, nodeToMove, nodeToMove]; | ||
|  |     } else { | ||
|  |       const [leftTree, rightTree, newParent] = recurse(parent); | ||
|  |       const nextSiblings = currentNode.getNextSiblings(); | ||
|  | 
 | ||
|  |       newParent.append(nodeToMove, ...nextSiblings); | ||
|  |       return [leftTree, rightTree, nodeToMove]; | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   const [leftTree, rightTree] = recurse(startNode); | ||
|  | 
 | ||
|  |   return [leftTree, rightTree]; | ||
|  | } | ||
|  | 
 | ||
|  | export function $findMatchingParent( | ||
|  |   startingNode: LexicalNode, | ||
|  |   findFn: (node: LexicalNode) => boolean, | ||
|  | ): LexicalNode | null { | ||
|  |   let curr: ElementNode | LexicalNode | null = startingNode; | ||
|  | 
 | ||
|  |   while (curr !== $getRoot() && curr != null) { | ||
|  |     if (findFn(curr)) { | ||
|  |       return curr; | ||
|  |     } | ||
|  | 
 | ||
|  |     curr = curr.getParent(); | ||
|  |   } | ||
|  | 
 | ||
|  |   return null; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param x - The element being tested | ||
|  |  * @returns Returns true if x is an HTML anchor tag, false otherwise | ||
|  |  */ | ||
|  | export function isHTMLAnchorElement(x: Node): x is HTMLAnchorElement { | ||
|  |   return isHTMLElement(x) && x.tagName === 'A'; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * @param x - The element being testing | ||
|  |  * @returns Returns true if x is an HTML element, false otherwise. | ||
|  |  */ | ||
|  | export function isHTMLElement(x: Node | EventTarget): x is HTMLElement { | ||
|  |   // @ts-ignore-next-line - strict check on nodeType here should filter out non-Element EventTarget implementors
 | ||
|  |   return x.nodeType === 1; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * | ||
|  |  * @param node - the Dom Node to check | ||
|  |  * @returns if the Dom Node is an inline node | ||
|  |  */ | ||
|  | export function isInlineDomNode(node: Node) { | ||
|  |   const inlineNodes = new RegExp( | ||
|  |     /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var|#text)$/, | ||
|  |     'i', | ||
|  |   ); | ||
|  |   return node.nodeName.match(inlineNodes) !== null; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * | ||
|  |  * @param node - the Dom Node to check | ||
|  |  * @returns if the Dom Node is a block node | ||
|  |  */ | ||
|  | export function isBlockDomNode(node: Node) { | ||
|  |   const blockNodes = new RegExp( | ||
|  |     /^(address|article|aside|blockquote|canvas|dd|div|dl|dt|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hr|li|main|nav|noscript|ol|p|pre|section|table|td|tfoot|ul|video)$/, | ||
|  |     'i', | ||
|  |   ); | ||
|  |   return node.nodeName.match(blockNodes) !== null; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * This function is for internal use of the library. | ||
|  |  * Please do not use it as it may change in the future. | ||
|  |  */ | ||
|  | export function INTERNAL_$isBlock( | ||
|  |   node: LexicalNode, | ||
|  | ): node is ElementNode | DecoratorNode<unknown> { | ||
|  |   if ($isRootNode(node) || ($isDecoratorNode(node) && !node.isInline())) { | ||
|  |     return true; | ||
|  |   } | ||
|  |   if (!$isElementNode(node) || $isRootOrShadowRoot(node)) { | ||
|  |     return false; | ||
|  |   } | ||
|  | 
 | ||
|  |   const firstChild = node.getFirstChild(); | ||
|  |   const isLeafElement = | ||
|  |     firstChild === null || | ||
|  |     $isLineBreakNode(firstChild) || | ||
|  |     $isTextNode(firstChild) || | ||
|  |     firstChild.isInline(); | ||
|  | 
 | ||
|  |   return !node.isInline() && node.canBeEmpty() !== false && isLeafElement; | ||
|  | } | ||
|  | 
 | ||
|  | export function $getAncestor<NodeType extends LexicalNode = LexicalNode>( | ||
|  |   node: LexicalNode, | ||
|  |   predicate: (ancestor: LexicalNode) => ancestor is NodeType, | ||
|  | ) { | ||
|  |   let parent = node; | ||
|  |   while (parent !== null && parent.getParent() !== null && !predicate(parent)) { | ||
|  |     parent = parent.getParentOrThrow(); | ||
|  |   } | ||
|  |   return predicate(parent) ? parent : null; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Utility function for accessing current active editor instance. | ||
|  |  * @returns Current active editor | ||
|  |  */ | ||
|  | export function $getEditor(): LexicalEditor { | ||
|  |   return getActiveEditor(); | ||
|  | } | ||
|  | 
 | ||
|  | /** @internal */ | ||
|  | export type TypeToNodeMap = Map<string, NodeMap>; | ||
|  | /** | ||
|  |  * @internal | ||
|  |  * Compute a cached Map of node type to nodes for a frozen EditorState | ||
|  |  */ | ||
|  | const cachedNodeMaps = new WeakMap<EditorState, TypeToNodeMap>(); | ||
|  | const EMPTY_TYPE_TO_NODE_MAP: TypeToNodeMap = new Map(); | ||
|  | export function getCachedTypeToNodeMap( | ||
|  |   editorState: EditorState, | ||
|  | ): TypeToNodeMap { | ||
|  |   // If this is a new Editor it may have a writable this._editorState
 | ||
|  |   // with only a 'root' entry.
 | ||
|  |   if (!editorState._readOnly && editorState.isEmpty()) { | ||
|  |     return EMPTY_TYPE_TO_NODE_MAP; | ||
|  |   } | ||
|  |   invariant( | ||
|  |     editorState._readOnly, | ||
|  |     'getCachedTypeToNodeMap called with a writable EditorState', | ||
|  |   ); | ||
|  |   let typeToNodeMap = cachedNodeMaps.get(editorState); | ||
|  |   if (!typeToNodeMap) { | ||
|  |     typeToNodeMap = new Map(); | ||
|  |     cachedNodeMaps.set(editorState, typeToNodeMap); | ||
|  |     for (const [nodeKey, node] of editorState._nodeMap) { | ||
|  |       const nodeType = node.__type; | ||
|  |       let nodeMap = typeToNodeMap.get(nodeType); | ||
|  |       if (!nodeMap) { | ||
|  |         nodeMap = new Map(); | ||
|  |         typeToNodeMap.set(nodeType, nodeMap); | ||
|  |       } | ||
|  |       nodeMap.set(nodeKey, node); | ||
|  |     } | ||
|  |   } | ||
|  |   return typeToNodeMap; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Returns a clone of a node using `node.constructor.clone()` followed by | ||
|  |  * `clone.afterCloneFrom(node)`. The resulting clone must have the same key, | ||
|  |  * parent/next/prev pointers, and other properties that are not set by | ||
|  |  * `node.constructor.clone` (format, style, etc.). This is primarily used by | ||
|  |  * {@link LexicalNode.getWritable} to create a writable version of an | ||
|  |  * existing node. The clone is the same logical node as the original node, | ||
|  |  * do not try and use this function to duplicate or copy an existing node. | ||
|  |  * | ||
|  |  * Does not mutate the EditorState. | ||
|  |  * @param node - The node to be cloned. | ||
|  |  * @returns The clone of the node. | ||
|  |  */ | ||
|  | export function $cloneWithProperties<T extends LexicalNode>(latestNode: T): T { | ||
|  |   const constructor = latestNode.constructor; | ||
|  |   const mutableNode = constructor.clone(latestNode) as T; | ||
|  |   mutableNode.afterCloneFrom(latestNode); | ||
|  |   if (__DEV__) { | ||
|  |     invariant( | ||
|  |       mutableNode.__key === latestNode.__key, | ||
|  |       "$cloneWithProperties: %s.clone(node) (with type '%s') did not return a node with the same key, make sure to specify node.__key as the last argument to the constructor", | ||
|  |       constructor.name, | ||
|  |       constructor.getType(), | ||
|  |     ); | ||
|  |     invariant( | ||
|  |       mutableNode.__parent === latestNode.__parent && | ||
|  |         mutableNode.__next === latestNode.__next && | ||
|  |         mutableNode.__prev === latestNode.__prev, | ||
|  |       "$cloneWithProperties: %s.clone(node) (with type '%s') overrided afterCloneFrom but did not call super.afterCloneFrom(prevNode)", | ||
|  |       constructor.name, | ||
|  |       constructor.getType(), | ||
|  |     ); | ||
|  |   } | ||
|  |   return mutableNode; | ||
|  | } |