206 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			5.8 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 {LexicalNode, Spread} from 'lexical';
 | |
| 
 | |
| import {$findMatchingParent} from '@lexical/utils';
 | |
| import invariant from 'lexical/shared/invariant';
 | |
| 
 | |
| import {
 | |
|   $createListItemNode,
 | |
|   $isListItemNode,
 | |
|   $isListNode,
 | |
|   ListItemNode,
 | |
|   ListNode,
 | |
| } from './';
 | |
| 
 | |
| /**
 | |
|  * Checks the depth of listNode from the root node.
 | |
|  * @param listNode - The ListNode to be checked.
 | |
|  * @returns The depth of the ListNode.
 | |
|  */
 | |
| export function $getListDepth(listNode: ListNode): number {
 | |
|   let depth = 1;
 | |
|   let parent = listNode.getParent();
 | |
| 
 | |
|   while (parent != null) {
 | |
|     if ($isListItemNode(parent)) {
 | |
|       const parentList = parent.getParent();
 | |
| 
 | |
|       if ($isListNode(parentList)) {
 | |
|         depth++;
 | |
|         parent = parentList.getParent();
 | |
|         continue;
 | |
|       }
 | |
|       invariant(false, 'A ListItemNode must have a ListNode for a parent.');
 | |
|     }
 | |
| 
 | |
|     return depth;
 | |
|   }
 | |
| 
 | |
|   return depth;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Finds the nearest ancestral ListNode and returns it, throws an invariant if listItem is not a ListItemNode.
 | |
|  * @param listItem - The node to be checked.
 | |
|  * @returns The ListNode found.
 | |
|  */
 | |
| export function $getTopListNode(listItem: LexicalNode): ListNode {
 | |
|   let list = listItem.getParent<ListNode>();
 | |
| 
 | |
|   if (!$isListNode(list)) {
 | |
|     invariant(false, 'A ListItemNode must have a ListNode for a parent.');
 | |
|   }
 | |
| 
 | |
|   let parent: ListNode | null = list;
 | |
| 
 | |
|   while (parent !== null) {
 | |
|     parent = parent.getParent();
 | |
| 
 | |
|     if ($isListNode(parent)) {
 | |
|       list = parent;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return list;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks if listItem has no child ListNodes and has no ListItemNode ancestors with siblings.
 | |
|  * @param listItem - the ListItemNode to be checked.
 | |
|  * @returns true if listItem has no child ListNode and no ListItemNode ancestors with siblings, false otherwise.
 | |
|  */
 | |
| export function $isLastItemInList(listItem: ListItemNode): boolean {
 | |
|   let isLast = true;
 | |
|   const firstChild = listItem.getFirstChild();
 | |
| 
 | |
|   if ($isListNode(firstChild)) {
 | |
|     return false;
 | |
|   }
 | |
|   let parent: ListItemNode | null = listItem;
 | |
| 
 | |
|   while (parent !== null) {
 | |
|     if ($isListItemNode(parent)) {
 | |
|       if (parent.getNextSiblings().length > 0) {
 | |
|         isLast = false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     parent = parent.getParent();
 | |
|   }
 | |
| 
 | |
|   return isLast;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * A recursive Depth-First Search (Postorder Traversal) that finds all of a node's children
 | |
|  * that are of type ListItemNode and returns them in an array.
 | |
|  * @param node - The ListNode to start the search.
 | |
|  * @returns An array containing all nodes of type ListItemNode found.
 | |
|  */
 | |
| // This should probably be $getAllChildrenOfType
 | |
| export function $getAllListItems(node: ListNode): Array<ListItemNode> {
 | |
|   let listItemNodes: Array<ListItemNode> = [];
 | |
|   const listChildren: Array<ListItemNode> = node
 | |
|     .getChildren()
 | |
|     .filter($isListItemNode);
 | |
| 
 | |
|   for (let i = 0; i < listChildren.length; i++) {
 | |
|     const listItemNode = listChildren[i];
 | |
|     const firstChild = listItemNode.getFirstChild();
 | |
| 
 | |
|     if ($isListNode(firstChild)) {
 | |
|       listItemNodes = listItemNodes.concat($getAllListItems(firstChild));
 | |
|     } else {
 | |
|       listItemNodes.push(listItemNode);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return listItemNodes;
 | |
| }
 | |
| 
 | |
| const NestedListNodeBrand: unique symbol = Symbol.for(
 | |
|   '@lexical/NestedListNodeBrand',
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * Checks to see if the passed node is a ListItemNode and has a ListNode as a child.
 | |
|  * @param node - The node to be checked.
 | |
|  * @returns true if the node is a ListItemNode and has a ListNode child, false otherwise.
 | |
|  */
 | |
| export function isNestedListNode(
 | |
|   node: LexicalNode | null | undefined,
 | |
| ): node is Spread<
 | |
|   {getFirstChild(): ListNode; [NestedListNodeBrand]: never},
 | |
|   ListItemNode
 | |
| > {
 | |
|   return $isListItemNode(node) && $isListNode(node.getFirstChild());
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Traverses up the tree and returns the first ListItemNode found.
 | |
|  * @param node - Node to start the search.
 | |
|  * @returns The first ListItemNode found, or null if none exist.
 | |
|  */
 | |
| export function $findNearestListItemNode(
 | |
|   node: LexicalNode,
 | |
| ): ListItemNode | null {
 | |
|   const matchingParent = $findMatchingParent(node, (parent) =>
 | |
|     $isListItemNode(parent),
 | |
|   );
 | |
|   return matchingParent as ListItemNode | null;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Takes a deeply nested ListNode or ListItemNode and traverses up the branch to delete the first
 | |
|  * ancestral ListNode (which could be the root ListNode) or ListItemNode with siblings, essentially
 | |
|  * bringing the deeply nested node up the branch once. Would remove sublist if it has siblings.
 | |
|  * Should not break ListItem -> List -> ListItem chain as empty List/ItemNodes should be removed on .remove().
 | |
|  * @param sublist - The nested ListNode or ListItemNode to be brought up the branch.
 | |
|  */
 | |
| export function $removeHighestEmptyListParent(
 | |
|   sublist: ListItemNode | ListNode,
 | |
| ) {
 | |
|   // Nodes may be repeatedly indented, to create deeply nested lists that each
 | |
|   // contain just one bullet.
 | |
|   // Our goal is to remove these (empty) deeply nested lists. The easiest
 | |
|   // way to do that is crawl back up the tree until we find a node that has siblings
 | |
|   // (e.g. is actually part of the list contents) and delete that, or delete
 | |
|   // the root of the list (if no list nodes have siblings.)
 | |
|   let emptyListPtr = sublist;
 | |
| 
 | |
|   while (
 | |
|     emptyListPtr.getNextSibling() == null &&
 | |
|     emptyListPtr.getPreviousSibling() == null
 | |
|   ) {
 | |
|     const parent = emptyListPtr.getParent<ListItemNode | ListNode>();
 | |
| 
 | |
|     if (
 | |
|       parent == null ||
 | |
|       !($isListItemNode(emptyListPtr) || $isListNode(emptyListPtr))
 | |
|     ) {
 | |
|       break;
 | |
|     }
 | |
| 
 | |
|     emptyListPtr = parent;
 | |
|   }
 | |
| 
 | |
|   emptyListPtr.remove();
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Wraps a node into a ListItemNode.
 | |
|  * @param node - The node to be wrapped into a ListItemNode
 | |
|  * @returns The ListItemNode which the passed node is wrapped in.
 | |
|  */
 | |
| export function $wrapInListItem(node: LexicalNode): ListItemNode {
 | |
|   const listItemWrapper = $createListItemNode();
 | |
|   return listItemWrapper.append(node);
 | |
| }
 |