608 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
		
		
			
		
	
	
			608 lines
		
	
	
		
			19 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 {
							 | 
						||
| 
								 | 
							
								  $cloneWithProperties,
							 | 
						||
| 
								 | 
							
								  $createParagraphNode,
							 | 
						||
| 
								 | 
							
								  $getPreviousSelection,
							 | 
						||
| 
								 | 
							
								  $getRoot,
							 | 
						||
| 
								 | 
							
								  $getSelection,
							 | 
						||
| 
								 | 
							
								  $isElementNode,
							 | 
						||
| 
								 | 
							
								  $isRangeSelection,
							 | 
						||
| 
								 | 
							
								  $isRootOrShadowRoot,
							 | 
						||
| 
								 | 
							
								  $isTextNode,
							 | 
						||
| 
								 | 
							
								  $setSelection,
							 | 
						||
| 
								 | 
							
								  $splitNode,
							 | 
						||
| 
								 | 
							
								  EditorState,
							 | 
						||
| 
								 | 
							
								  ElementNode,
							 | 
						||
| 
								 | 
							
								  Klass,
							 | 
						||
| 
								 | 
							
								  LexicalEditor,
							 | 
						||
| 
								 | 
							
								  LexicalNode,
							 | 
						||
| 
								 | 
							
								} from 'lexical';
							 | 
						||
| 
								 | 
							
								// This underscore postfixing is used as a hotfix so we do not
							 | 
						||
| 
								 | 
							
								// export shared types from this module #5918
							 | 
						||
| 
								 | 
							
								import {CAN_USE_DOM as CAN_USE_DOM_} from 'lexical/shared/canUseDOM';
							 | 
						||
| 
								 | 
							
								import {
							 | 
						||
| 
								 | 
							
								  CAN_USE_BEFORE_INPUT as CAN_USE_BEFORE_INPUT_,
							 | 
						||
| 
								 | 
							
								  IS_ANDROID as IS_ANDROID_,
							 | 
						||
| 
								 | 
							
								  IS_ANDROID_CHROME as IS_ANDROID_CHROME_,
							 | 
						||
| 
								 | 
							
								  IS_APPLE as IS_APPLE_,
							 | 
						||
| 
								 | 
							
								  IS_APPLE_WEBKIT as IS_APPLE_WEBKIT_,
							 | 
						||
| 
								 | 
							
								  IS_CHROME as IS_CHROME_,
							 | 
						||
| 
								 | 
							
								  IS_FIREFOX as IS_FIREFOX_,
							 | 
						||
| 
								 | 
							
								  IS_IOS as IS_IOS_,
							 | 
						||
| 
								 | 
							
								  IS_SAFARI as IS_SAFARI_,
							 | 
						||
| 
								 | 
							
								} from 'lexical/shared/environment';
							 | 
						||
| 
								 | 
							
								import invariant from 'lexical/shared/invariant';
							 | 
						||
| 
								 | 
							
								import normalizeClassNames from 'lexical/shared/normalizeClassNames';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export {default as markSelection} from './markSelection';
							 | 
						||
| 
								 | 
							
								export {default as mergeRegister} from './mergeRegister';
							 | 
						||
| 
								 | 
							
								export {default as positionNodeOnRange} from './positionNodeOnRange';
							 | 
						||
| 
								 | 
							
								export {
							 | 
						||
| 
								 | 
							
								  $splitNode,
							 | 
						||
| 
								 | 
							
								  isBlockDomNode,
							 | 
						||
| 
								 | 
							
								  isHTMLAnchorElement,
							 | 
						||
| 
								 | 
							
								  isHTMLElement,
							 | 
						||
| 
								 | 
							
								  isInlineDomNode,
							 | 
						||
| 
								 | 
							
								} from 'lexical';
							 | 
						||
| 
								 | 
							
								// Hotfix to export these with inlined types #5918
							 | 
						||
| 
								 | 
							
								export const CAN_USE_BEFORE_INPUT: boolean = CAN_USE_BEFORE_INPUT_;
							 | 
						||
| 
								 | 
							
								export const CAN_USE_DOM: boolean = CAN_USE_DOM_;
							 | 
						||
| 
								 | 
							
								export const IS_ANDROID: boolean = IS_ANDROID_;
							 | 
						||
| 
								 | 
							
								export const IS_ANDROID_CHROME: boolean = IS_ANDROID_CHROME_;
							 | 
						||
| 
								 | 
							
								export const IS_APPLE: boolean = IS_APPLE_;
							 | 
						||
| 
								 | 
							
								export const IS_APPLE_WEBKIT: boolean = IS_APPLE_WEBKIT_;
							 | 
						||
| 
								 | 
							
								export const IS_CHROME: boolean = IS_CHROME_;
							 | 
						||
| 
								 | 
							
								export const IS_FIREFOX: boolean = IS_FIREFOX_;
							 | 
						||
| 
								 | 
							
								export const IS_IOS: boolean = IS_IOS_;
							 | 
						||
| 
								 | 
							
								export const IS_SAFARI: boolean = IS_SAFARI_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export type DFSNode = Readonly<{
							 | 
						||
| 
								 | 
							
								  depth: number;
							 | 
						||
| 
								 | 
							
								  node: LexicalNode;
							 | 
						||
| 
								 | 
							
								}>;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Takes an HTML element and adds the classNames passed within an array,
							 | 
						||
| 
								 | 
							
								 * ignoring any non-string types. A space can be used to add multiple classes
							 | 
						||
| 
								 | 
							
								 * eg. addClassNamesToElement(element, ['element-inner active', true, null])
							 | 
						||
| 
								 | 
							
								 * will add both 'element-inner' and 'active' as classes to that element.
							 | 
						||
| 
								 | 
							
								 * @param element - The element in which the classes are added
							 | 
						||
| 
								 | 
							
								 * @param classNames - An array defining the class names to add to the element
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function addClassNamesToElement(
							 | 
						||
| 
								 | 
							
								  element: HTMLElement,
							 | 
						||
| 
								 | 
							
								  ...classNames: Array<typeof undefined | boolean | null | string>
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const classesToAdd = normalizeClassNames(...classNames);
							 | 
						||
| 
								 | 
							
								  if (classesToAdd.length > 0) {
							 | 
						||
| 
								 | 
							
								    element.classList.add(...classesToAdd);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Takes an HTML element and removes the classNames passed within an array,
							 | 
						||
| 
								 | 
							
								 * ignoring any non-string types. A space can be used to remove multiple classes
							 | 
						||
| 
								 | 
							
								 * eg. removeClassNamesFromElement(element, ['active small', true, null])
							 | 
						||
| 
								 | 
							
								 * will remove both the 'active' and 'small' classes from that element.
							 | 
						||
| 
								 | 
							
								 * @param element - The element in which the classes are removed
							 | 
						||
| 
								 | 
							
								 * @param classNames - An array defining the class names to remove from the element
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function removeClassNamesFromElement(
							 | 
						||
| 
								 | 
							
								  element: HTMLElement,
							 | 
						||
| 
								 | 
							
								  ...classNames: Array<typeof undefined | boolean | null | string>
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const classesToRemove = normalizeClassNames(...classNames);
							 | 
						||
| 
								 | 
							
								  if (classesToRemove.length > 0) {
							 | 
						||
| 
								 | 
							
								    element.classList.remove(...classesToRemove);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Returns true if the file type matches the types passed within the acceptableMimeTypes array, false otherwise.
							 | 
						||
| 
								 | 
							
								 * The types passed must be strings and are CASE-SENSITIVE.
							 | 
						||
| 
								 | 
							
								 * eg. if file is of type 'text' and acceptableMimeTypes = ['TEXT', 'IMAGE'] the function will return false.
							 | 
						||
| 
								 | 
							
								 * @param file - The file you want to type check.
							 | 
						||
| 
								 | 
							
								 * @param acceptableMimeTypes - An array of strings of types which the file is checked against.
							 | 
						||
| 
								 | 
							
								 * @returns true if the file is an acceptable mime type, false otherwise.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function isMimeType(
							 | 
						||
| 
								 | 
							
								  file: File,
							 | 
						||
| 
								 | 
							
								  acceptableMimeTypes: Array<string>,
							 | 
						||
| 
								 | 
							
								): boolean {
							 | 
						||
| 
								 | 
							
								  for (const acceptableType of acceptableMimeTypes) {
							 | 
						||
| 
								 | 
							
								    if (file.type.startsWith(acceptableType)) {
							 | 
						||
| 
								 | 
							
								      return true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return false;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Lexical File Reader with:
							 | 
						||
| 
								 | 
							
								 *  1. MIME type support
							 | 
						||
| 
								 | 
							
								 *  2. batched results (HistoryPlugin compatibility)
							 | 
						||
| 
								 | 
							
								 *  3. Order aware (respects the order when multiple Files are passed)
							 | 
						||
| 
								 | 
							
								 *
							 | 
						||
| 
								 | 
							
								 * const filesResult = await mediaFileReader(files, ['image/']);
							 | 
						||
| 
								 | 
							
								 * filesResult.forEach(file => editor.dispatchCommand('INSERT_IMAGE', \\{
							 | 
						||
| 
								 | 
							
								 *   src: file.result,
							 | 
						||
| 
								 | 
							
								 * \\}));
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function mediaFileReader(
							 | 
						||
| 
								 | 
							
								  files: Array<File>,
							 | 
						||
| 
								 | 
							
								  acceptableMimeTypes: Array<string>,
							 | 
						||
| 
								 | 
							
								): Promise<Array<{file: File; result: string}>> {
							 | 
						||
| 
								 | 
							
								  const filesIterator = files[Symbol.iterator]();
							 | 
						||
| 
								 | 
							
								  return new Promise((resolve, reject) => {
							 | 
						||
| 
								 | 
							
								    const processed: Array<{file: File; result: string}> = [];
							 | 
						||
| 
								 | 
							
								    const handleNextFile = () => {
							 | 
						||
| 
								 | 
							
								      const {done, value: file} = filesIterator.next();
							 | 
						||
| 
								 | 
							
								      if (done) {
							 | 
						||
| 
								 | 
							
								        return resolve(processed);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      const fileReader = new FileReader();
							 | 
						||
| 
								 | 
							
								      fileReader.addEventListener('error', reject);
							 | 
						||
| 
								 | 
							
								      fileReader.addEventListener('load', () => {
							 | 
						||
| 
								 | 
							
								        const result = fileReader.result;
							 | 
						||
| 
								 | 
							
								        if (typeof result === 'string') {
							 | 
						||
| 
								 | 
							
								          processed.push({file, result});
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        handleNextFile();
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								      if (isMimeType(file, acceptableMimeTypes)) {
							 | 
						||
| 
								 | 
							
								        fileReader.readAsDataURL(file);
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        handleNextFile();
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    };
							 | 
						||
| 
								 | 
							
								    handleNextFile();
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * "Depth-First Search" starts at the root/top node of a tree and goes as far as it can down a branch end
							 | 
						||
| 
								 | 
							
								 * before backtracking and finding a new path. Consider solving a maze by hugging either wall, moving down a
							 | 
						||
| 
								 | 
							
								 * branch until you hit a dead-end (leaf) and backtracking to find the nearest branching path and repeat.
							 | 
						||
| 
								 | 
							
								 * It will then return all the nodes found in the search in an array of objects.
							 | 
						||
| 
								 | 
							
								 * @param startingNode - The node to start the search, if ommitted, it will start at the root node.
							 | 
						||
| 
								 | 
							
								 * @param endingNode - The node to end the search, if ommitted, it will find all descendants of the startingNode.
							 | 
						||
| 
								 | 
							
								 * @returns An array of objects of all the nodes found by the search, including their depth into the tree.
							 | 
						||
| 
								 | 
							
								 * \\{depth: number, node: LexicalNode\\} It will always return at least 1 node (the ending node) so long as it exists
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $dfs(
							 | 
						||
| 
								 | 
							
								  startingNode?: LexicalNode,
							 | 
						||
| 
								 | 
							
								  endingNode?: LexicalNode,
							 | 
						||
| 
								 | 
							
								): Array<DFSNode> {
							 | 
						||
| 
								 | 
							
								  const nodes = [];
							 | 
						||
| 
								 | 
							
								  const start = (startingNode || $getRoot()).getLatest();
							 | 
						||
| 
								 | 
							
								  const end =
							 | 
						||
| 
								 | 
							
								    endingNode ||
							 | 
						||
| 
								 | 
							
								    ($isElementNode(start) ? start.getLastDescendant() || start : start);
							 | 
						||
| 
								 | 
							
								  let node: LexicalNode | null = start;
							 | 
						||
| 
								 | 
							
								  let depth = $getDepth(node);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  while (node !== null && !node.is(end)) {
							 | 
						||
| 
								 | 
							
								    nodes.push({depth, node});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ($isElementNode(node) && node.getChildrenSize() > 0) {
							 | 
						||
| 
								 | 
							
								      node = node.getFirstChild();
							 | 
						||
| 
								 | 
							
								      depth++;
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      // Find immediate sibling or nearest parent sibling
							 | 
						||
| 
								 | 
							
								      let sibling = null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      while (sibling === null && node !== null) {
							 | 
						||
| 
								 | 
							
								        sibling = node.getNextSibling();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (sibling === null) {
							 | 
						||
| 
								 | 
							
								          node = node.getParent();
							 | 
						||
| 
								 | 
							
								          depth--;
							 | 
						||
| 
								 | 
							
								        } else {
							 | 
						||
| 
								 | 
							
								          node = sibling;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (node !== null && node.is(end)) {
							 | 
						||
| 
								 | 
							
								    nodes.push({depth, node});
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return nodes;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function $getDepth(node: LexicalNode): number {
							 | 
						||
| 
								 | 
							
								  let innerNode: LexicalNode | null = node;
							 | 
						||
| 
								 | 
							
								  let depth = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  while ((innerNode = innerNode.getParent()) !== null) {
							 | 
						||
| 
								 | 
							
								    depth++;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return depth;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Performs a right-to-left preorder tree traversal.
							 | 
						||
| 
								 | 
							
								 * From the starting node it goes to the rightmost child, than backtracks to paret and finds new rightmost path.
							 | 
						||
| 
								 | 
							
								 * It will return the next node in traversal sequence after the startingNode.
							 | 
						||
| 
								 | 
							
								 * The traversal is similar to $dfs functions above, but the nodes are visited right-to-left, not left-to-right.
							 | 
						||
| 
								 | 
							
								 * @param startingNode - The node to start the search.
							 | 
						||
| 
								 | 
							
								 * @returns The next node in pre-order right to left traversal sequence or `null`, if the node does not exist
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $getNextRightPreorderNode(
							 | 
						||
| 
								 | 
							
								  startingNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								): LexicalNode | null {
							 | 
						||
| 
								 | 
							
								  let node: LexicalNode | null = startingNode;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if ($isElementNode(node) && node.getChildrenSize() > 0) {
							 | 
						||
| 
								 | 
							
								    node = node.getLastChild();
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    let sibling = null;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    while (sibling === null && node !== null) {
							 | 
						||
| 
								 | 
							
								      sibling = node.getPreviousSibling();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (sibling === null) {
							 | 
						||
| 
								 | 
							
								        node = node.getParent();
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        node = sibling;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return node;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Takes a node and traverses up its ancestors (toward the root node)
							 | 
						||
| 
								 | 
							
								 * in order to find a specific type of node.
							 | 
						||
| 
								 | 
							
								 * @param node - the node to begin searching.
							 | 
						||
| 
								 | 
							
								 * @param klass - an instance of the type of node to look for.
							 | 
						||
| 
								 | 
							
								 * @returns the node of type klass that was passed, or null if none exist.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $getNearestNodeOfType<T extends ElementNode>(
							 | 
						||
| 
								 | 
							
								  node: LexicalNode,
							 | 
						||
| 
								 | 
							
								  klass: Klass<T>,
							 | 
						||
| 
								 | 
							
								): T | null {
							 | 
						||
| 
								 | 
							
								  let parent: ElementNode | LexicalNode | null = node;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  while (parent != null) {
							 | 
						||
| 
								 | 
							
								    if (parent instanceof klass) {
							 | 
						||
| 
								 | 
							
								      return parent as T;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    parent = parent.getParent();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return null;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Returns the element node of the nearest ancestor, otherwise throws an error.
							 | 
						||
| 
								 | 
							
								 * @param startNode - The starting node of the search
							 | 
						||
| 
								 | 
							
								 * @returns The ancestor node found
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $getNearestBlockElementAncestorOrThrow(
							 | 
						||
| 
								 | 
							
								  startNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								): ElementNode {
							 | 
						||
| 
								 | 
							
								  const blockNode = $findMatchingParent(
							 | 
						||
| 
								 | 
							
								    startNode,
							 | 
						||
| 
								 | 
							
								    (node) => $isElementNode(node) && !node.isInline(),
							 | 
						||
| 
								 | 
							
								  );
							 | 
						||
| 
								 | 
							
								  if (!$isElementNode(blockNode)) {
							 | 
						||
| 
								 | 
							
								    invariant(
							 | 
						||
| 
								 | 
							
								      false,
							 | 
						||
| 
								 | 
							
								      'Expected node %s to have closest block element node.',
							 | 
						||
| 
								 | 
							
								      startNode.__key,
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return blockNode;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export type DOMNodeToLexicalConversion = (element: Node) => LexicalNode;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export type DOMNodeToLexicalConversionMap = Record<
							 | 
						||
| 
								 | 
							
								  string,
							 | 
						||
| 
								 | 
							
								  DOMNodeToLexicalConversion
							 | 
						||
| 
								 | 
							
								>;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Starts with a node and moves up the tree (toward the root node) to find a matching node based on
							 | 
						||
| 
								 | 
							
								 * the search parameters of the findFn. (Consider JavaScripts' .find() function where a testing function must be
							 | 
						||
| 
								 | 
							
								 * passed as an argument. eg. if( (node) => node.__type === 'div') ) return true; otherwise return false
							 | 
						||
| 
								 | 
							
								 * @param startingNode - The node where the search starts.
							 | 
						||
| 
								 | 
							
								 * @param findFn - A testing function that returns true if the current node satisfies the testing parameters.
							 | 
						||
| 
								 | 
							
								 * @returns A parent node that matches the findFn parameters, or null if one wasn't found.
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export const $findMatchingParent: {
							 | 
						||
| 
								 | 
							
								  <T extends LexicalNode>(
							 | 
						||
| 
								 | 
							
								    startingNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								    findFn: (node: LexicalNode) => node is T,
							 | 
						||
| 
								 | 
							
								  ): T | null;
							 | 
						||
| 
								 | 
							
								  (
							 | 
						||
| 
								 | 
							
								    startingNode: LexicalNode,
							 | 
						||
| 
								 | 
							
								    findFn: (node: LexicalNode) => boolean,
							 | 
						||
| 
								 | 
							
								  ): LexicalNode | null;
							 | 
						||
| 
								 | 
							
								} = (
							 | 
						||
| 
								 | 
							
								  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;
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Attempts to resolve nested element nodes of the same type into a single node of that type.
							 | 
						||
| 
								 | 
							
								 * It is generally used for marks/commenting
							 | 
						||
| 
								 | 
							
								 * @param editor - The lexical editor
							 | 
						||
| 
								 | 
							
								 * @param targetNode - The target for the nested element to be extracted from.
							 | 
						||
| 
								 | 
							
								 * @param cloneNode - See {@link $createMarkNode}
							 | 
						||
| 
								 | 
							
								 * @param handleOverlap - Handles any overlap between the node to extract and the targetNode
							 | 
						||
| 
								 | 
							
								 * @returns The lexical editor
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function registerNestedElementResolver<N extends ElementNode>(
							 | 
						||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						||
| 
								 | 
							
								  targetNode: Klass<N>,
							 | 
						||
| 
								 | 
							
								  cloneNode: (from: N) => N,
							 | 
						||
| 
								 | 
							
								  handleOverlap: (from: N, to: N) => void,
							 | 
						||
| 
								 | 
							
								): () => void {
							 | 
						||
| 
								 | 
							
								  const $isTargetNode = (node: LexicalNode | null | undefined): node is N => {
							 | 
						||
| 
								 | 
							
								    return node instanceof targetNode;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const $findMatch = (node: N): {child: ElementNode; parent: N} | null => {
							 | 
						||
| 
								 | 
							
								    // First validate we don't have any children that are of the target,
							 | 
						||
| 
								 | 
							
								    // as we need to handle them first.
							 | 
						||
| 
								 | 
							
								    const children = node.getChildren();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < children.length; i++) {
							 | 
						||
| 
								 | 
							
								      const child = children[i];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if ($isTargetNode(child)) {
							 | 
						||
| 
								 | 
							
								        return null;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let parentNode: N | null = node;
							 | 
						||
| 
								 | 
							
								    let childNode = node;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    while (parentNode !== null) {
							 | 
						||
| 
								 | 
							
								      childNode = parentNode;
							 | 
						||
| 
								 | 
							
								      parentNode = parentNode.getParent();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if ($isTargetNode(parentNode)) {
							 | 
						||
| 
								 | 
							
								        return {child: childNode, parent: parentNode};
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return null;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  const $elementNodeTransform = (node: N) => {
							 | 
						||
| 
								 | 
							
								    const match = $findMatch(node);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (match !== null) {
							 | 
						||
| 
								 | 
							
								      const {child, parent} = match;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      // Simple path, we can move child out and siblings into a new parent.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (child.is(node)) {
							 | 
						||
| 
								 | 
							
								        handleOverlap(parent, node);
							 | 
						||
| 
								 | 
							
								        const nextSiblings = child.getNextSiblings();
							 | 
						||
| 
								 | 
							
								        const nextSiblingsLength = nextSiblings.length;
							 | 
						||
| 
								 | 
							
								        parent.insertAfter(child);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (nextSiblingsLength !== 0) {
							 | 
						||
| 
								 | 
							
								          const newParent = cloneNode(parent);
							 | 
						||
| 
								 | 
							
								          child.insertAfter(newParent);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								          for (let i = 0; i < nextSiblingsLength; i++) {
							 | 
						||
| 
								 | 
							
								            newParent.append(nextSiblings[i]);
							 | 
						||
| 
								 | 
							
								          }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        if (!parent.canBeEmpty() && parent.getChildrenSize() === 0) {
							 | 
						||
| 
								 | 
							
								          parent.remove();
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        // Complex path, we have a deep node that isn't a child of the
							 | 
						||
| 
								 | 
							
								        // target parent.
							 | 
						||
| 
								 | 
							
								        // TODO: implement this functionality
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return editor.registerNodeTransform(targetNode, $elementNodeTransform);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Clones the editor and marks it as dirty to be reconciled. If there was a selection,
							 | 
						||
| 
								 | 
							
								 * it would be set back to its previous state, or null otherwise.
							 | 
						||
| 
								 | 
							
								 * @param editor - The lexical editor
							 | 
						||
| 
								 | 
							
								 * @param editorState - The editor's state
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $restoreEditorState(
							 | 
						||
| 
								 | 
							
								  editor: LexicalEditor,
							 | 
						||
| 
								 | 
							
								  editorState: EditorState,
							 | 
						||
| 
								 | 
							
								): void {
							 | 
						||
| 
								 | 
							
								  const FULL_RECONCILE = 2;
							 | 
						||
| 
								 | 
							
								  const nodeMap = new Map();
							 | 
						||
| 
								 | 
							
								  const activeEditorState = editor._pendingEditorState;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (const [key, node] of editorState._nodeMap) {
							 | 
						||
| 
								 | 
							
								    nodeMap.set(key, $cloneWithProperties(node));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (activeEditorState) {
							 | 
						||
| 
								 | 
							
								    activeEditorState._nodeMap = nodeMap;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  editor._dirtyType = FULL_RECONCILE;
							 | 
						||
| 
								 | 
							
								  const selection = editorState._selection;
							 | 
						||
| 
								 | 
							
								  $setSelection(selection === null ? null : selection.clone());
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
							 | 
						||
| 
								 | 
							
								 * the node will be appended there, otherwise, it will be inserted before the insertion area.
							 | 
						||
| 
								 | 
							
								 * If there is no selection where the node is to be inserted, it will be appended after any current nodes
							 | 
						||
| 
								 | 
							
								 * within the tree, as a child of the root node. A paragraph node will then be added after the inserted node and selected.
							 | 
						||
| 
								 | 
							
								 * @param node - The node to be inserted
							 | 
						||
| 
								 | 
							
								 * @returns The node after its insertion
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T {
							 | 
						||
| 
								 | 
							
								  const selection = $getSelection() || $getPreviousSelection();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if ($isRangeSelection(selection)) {
							 | 
						||
| 
								 | 
							
								    const {focus} = selection;
							 | 
						||
| 
								 | 
							
								    const focusNode = focus.getNode();
							 | 
						||
| 
								 | 
							
								    const focusOffset = focus.offset;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ($isRootOrShadowRoot(focusNode)) {
							 | 
						||
| 
								 | 
							
								      const focusChild = focusNode.getChildAtIndex(focusOffset);
							 | 
						||
| 
								 | 
							
								      if (focusChild == null) {
							 | 
						||
| 
								 | 
							
								        focusNode.append(node);
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        focusChild.insertBefore(node);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      node.selectNext();
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      let splitNode: ElementNode;
							 | 
						||
| 
								 | 
							
								      let splitOffset: number;
							 | 
						||
| 
								 | 
							
								      if ($isTextNode(focusNode)) {
							 | 
						||
| 
								 | 
							
								        splitNode = focusNode.getParentOrThrow();
							 | 
						||
| 
								 | 
							
								        splitOffset = focusNode.getIndexWithinParent();
							 | 
						||
| 
								 | 
							
								        if (focusOffset > 0) {
							 | 
						||
| 
								 | 
							
								          splitOffset += 1;
							 | 
						||
| 
								 | 
							
								          focusNode.splitText(focusOffset);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      } else {
							 | 
						||
| 
								 | 
							
								        splitNode = focusNode;
							 | 
						||
| 
								 | 
							
								        splitOffset = focusOffset;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								      const [, rightTree] = $splitNode(splitNode, splitOffset);
							 | 
						||
| 
								 | 
							
								      rightTree.insertBefore(node);
							 | 
						||
| 
								 | 
							
								      rightTree.selectStart();
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    if (selection != null) {
							 | 
						||
| 
								 | 
							
								      const nodes = selection.getNodes();
							 | 
						||
| 
								 | 
							
								      nodes[nodes.length - 1].getTopLevelElementOrThrow().insertAfter(node);
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      const root = $getRoot();
							 | 
						||
| 
								 | 
							
								      root.append(node);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    const paragraphNode = $createParagraphNode();
							 | 
						||
| 
								 | 
							
								    node.insertAfter(paragraphNode);
							 | 
						||
| 
								 | 
							
								    paragraphNode.select();
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return node.getLatest();
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
							 | 
						||
| 
								 | 
							
								 * @param node - Node to be wrapped.
							 | 
						||
| 
								 | 
							
								 * @param createElementNode - Creates a new lexical element to wrap the to-be-wrapped node and returns it.
							 | 
						||
| 
								 | 
							
								 * @returns A new lexical element with the previous node appended within (as a child, including its children).
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $wrapNodeInElement(
							 | 
						||
| 
								 | 
							
								  node: LexicalNode,
							 | 
						||
| 
								 | 
							
								  createElementNode: () => ElementNode,
							 | 
						||
| 
								 | 
							
								): ElementNode {
							 | 
						||
| 
								 | 
							
								  const elementNode = createElementNode();
							 | 
						||
| 
								 | 
							
								  node.replace(elementNode);
							 | 
						||
| 
								 | 
							
								  elementNode.append(node);
							 | 
						||
| 
								 | 
							
								  return elementNode;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						||
| 
								 | 
							
								type ObjectKlass<T> = new (...args: any[]) => T;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * @param object = The instance of the type
							 | 
						||
| 
								 | 
							
								 * @param objectClass = The class of the type
							 | 
						||
| 
								 | 
							
								 * @returns Whether the object is has the same Klass of the objectClass, ignoring the difference across window (e.g. different iframs)
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function objectKlassEquals<T>(
							 | 
						||
| 
								 | 
							
								  object: unknown,
							 | 
						||
| 
								 | 
							
								  objectClass: ObjectKlass<T>,
							 | 
						||
| 
								 | 
							
								): boolean {
							 | 
						||
| 
								 | 
							
								  return object !== null
							 | 
						||
| 
								 | 
							
								    ? Object.getPrototypeOf(object).constructor.name === objectClass.name
							 | 
						||
| 
								 | 
							
								    : false;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Filter the nodes
							 | 
						||
| 
								 | 
							
								 * @param nodes Array of nodes that needs to be filtered
							 | 
						||
| 
								 | 
							
								 * @param filterFn A filter function that returns node if the current node satisfies the condition otherwise null
							 | 
						||
| 
								 | 
							
								 * @returns Array of filtered nodes
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export function $filter<T>(
							 | 
						||
| 
								 | 
							
								  nodes: Array<LexicalNode>,
							 | 
						||
| 
								 | 
							
								  filterFn: (node: LexicalNode) => null | T,
							 | 
						||
| 
								 | 
							
								): Array<T> {
							 | 
						||
| 
								 | 
							
								  const result: T[] = [];
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < nodes.length; i++) {
							 | 
						||
| 
								 | 
							
								    const node = filterFn(nodes[i]);
							 | 
						||
| 
								 | 
							
								    if (node !== null) {
							 | 
						||
| 
								 | 
							
								      result.push(node);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return result;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Appends the node before the first child of the parent node
							 | 
						||
| 
								 | 
							
								 * @param parent A parent node
							 | 
						||
| 
								 | 
							
								 * @param node Node that needs to be appended
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $insertFirst(parent: ElementNode, node: LexicalNode): void {
							 | 
						||
| 
								 | 
							
								  const firstChild = parent.getFirstChild();
							 | 
						||
| 
								 | 
							
								  if (firstChild !== null) {
							 | 
						||
| 
								 | 
							
								    firstChild.insertBefore(node);
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    parent.append(node);
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Calculates the zoom level of an element as a result of using
							 | 
						||
| 
								 | 
							
								 * css zoom property.
							 | 
						||
| 
								 | 
							
								 * @param element
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function calculateZoomLevel(element: Element | null): number {
							 | 
						||
| 
								 | 
							
								  if (IS_FIREFOX) {
							 | 
						||
| 
								 | 
							
								    return 1;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  let zoom = 1;
							 | 
						||
| 
								 | 
							
								  while (element) {
							 | 
						||
| 
								 | 
							
								    zoom *= Number(window.getComputedStyle(element).getPropertyValue('zoom'));
							 | 
						||
| 
								 | 
							
								    element = element.parentElement;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  return zoom;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/**
							 | 
						||
| 
								 | 
							
								 * Checks if the editor is a nested editor created by LexicalNestedComposer
							 | 
						||
| 
								 | 
							
								 */
							 | 
						||
| 
								 | 
							
								export function $isEditorIsNestedEditor(editor: LexicalEditor): boolean {
							 | 
						||
| 
								 | 
							
								  return editor._parentEditor !== null;
							 | 
						||
| 
								 | 
							
								}
							 |