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;
 | |
| }
 |