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