2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {LexicalNode, Spread} from 'lexical';
11 import {$findMatchingParent} from '@lexical/utils';
12 import invariant from 'lexical/shared/invariant';
23 * Checks the depth of listNode from the root node.
24 * @param listNode - The ListNode to be checked.
25 * @returns The depth of the ListNode.
27 export function $getListDepth(listNode: ListNode): number {
29 let parent = listNode.getParent();
31 while (parent != null) {
32 if ($isListItemNode(parent)) {
33 const parentList = parent.getParent();
35 if ($isListNode(parentList)) {
37 parent = parentList.getParent();
40 invariant(false, 'A ListItemNode must have a ListNode for a parent.');
50 * Finds the nearest ancestral ListNode and returns it, throws an invariant if listItem is not a ListItemNode.
51 * @param listItem - The node to be checked.
52 * @returns The ListNode found.
54 export function $getTopListNode(listItem: LexicalNode): ListNode {
55 let list = listItem.getParent<ListNode>();
57 if (!$isListNode(list)) {
58 invariant(false, 'A ListItemNode must have a ListNode for a parent.');
61 let parent: ListNode | null = list;
63 while (parent !== null) {
64 parent = parent.getParent();
66 if ($isListNode(parent)) {
75 * Checks if listItem has no child ListNodes and has no ListItemNode ancestors with siblings.
76 * @param listItem - the ListItemNode to be checked.
77 * @returns true if listItem has no child ListNode and no ListItemNode ancestors with siblings, false otherwise.
79 export function $isLastItemInList(listItem: ListItemNode): boolean {
81 const firstChild = listItem.getFirstChild();
83 if ($isListNode(firstChild)) {
86 let parent: ListItemNode | null = listItem;
88 while (parent !== null) {
89 if ($isListItemNode(parent)) {
90 if (parent.getNextSiblings().length > 0) {
95 parent = parent.getParent();
102 * A recursive Depth-First Search (Postorder Traversal) that finds all of a node's children
103 * that are of type ListItemNode and returns them in an array.
104 * @param node - The ListNode to start the search.
105 * @returns An array containing all nodes of type ListItemNode found.
107 // This should probably be $getAllChildrenOfType
108 export function $getAllListItems(node: ListNode): Array<ListItemNode> {
109 let listItemNodes: Array<ListItemNode> = [];
110 const listChildren: Array<ListItemNode> = node
112 .filter($isListItemNode);
114 for (let i = 0; i < listChildren.length; i++) {
115 const listItemNode = listChildren[i];
116 const firstChild = listItemNode.getFirstChild();
118 if ($isListNode(firstChild)) {
119 listItemNodes = listItemNodes.concat($getAllListItems(firstChild));
121 listItemNodes.push(listItemNode);
125 return listItemNodes;
128 const NestedListNodeBrand: unique symbol = Symbol.for(
129 '@lexical/NestedListNodeBrand',
133 * Checks to see if the passed node is a ListItemNode and has a ListNode as a child.
134 * @param node - The node to be checked.
135 * @returns true if the node is a ListItemNode and has a ListNode child, false otherwise.
137 export function isNestedListNode(
138 node: LexicalNode | null | undefined,
140 {getFirstChild(): ListNode; [NestedListNodeBrand]: never},
143 return $isListItemNode(node) && $isListNode(node.getFirstChild());
147 * Traverses up the tree and returns the first ListItemNode found.
148 * @param node - Node to start the search.
149 * @returns The first ListItemNode found, or null if none exist.
151 export function $findNearestListItemNode(
153 ): ListItemNode | null {
154 const matchingParent = $findMatchingParent(node, (parent) =>
155 $isListItemNode(parent),
157 return matchingParent as ListItemNode | null;
161 * Takes a deeply nested ListNode or ListItemNode and traverses up the branch to delete the first
162 * ancestral ListNode (which could be the root ListNode) or ListItemNode with siblings, essentially
163 * bringing the deeply nested node up the branch once. Would remove sublist if it has siblings.
164 * Should not break ListItem -> List -> ListItem chain as empty List/ItemNodes should be removed on .remove().
165 * @param sublist - The nested ListNode or ListItemNode to be brought up the branch.
167 export function $removeHighestEmptyListParent(
168 sublist: ListItemNode | ListNode,
170 // Nodes may be repeatedly indented, to create deeply nested lists that each
171 // contain just one bullet.
172 // Our goal is to remove these (empty) deeply nested lists. The easiest
173 // way to do that is crawl back up the tree until we find a node that has siblings
174 // (e.g. is actually part of the list contents) and delete that, or delete
175 // the root of the list (if no list nodes have siblings.)
176 let emptyListPtr = sublist;
179 emptyListPtr.getNextSibling() == null &&
180 emptyListPtr.getPreviousSibling() == null
182 const parent = emptyListPtr.getParent<ListItemNode | ListNode>();
186 !($isListItemNode(emptyListPtr) || $isListNode(emptyListPtr))
191 emptyListPtr = parent;
194 emptyListPtr.remove();
198 * Wraps a node into a ListItemNode.
199 * @param node - The node to be wrapped into a ListItemNode
200 * @returns The ListItemNode which the passed node is wrapped in.
202 export function $wrapInListItem(node: LexicalNode): ListItemNode {
203 const listItemWrapper = $createListItemNode();
204 return listItemWrapper.append(node);