1 import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical";
2 import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
3 import {$sortNodes, nodeHasInset} from "./nodes";
4 import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
7 export function $nestListItem(node: ListItemNode): ListItemNode {
8 const list = node.getParent();
9 if (!$isListNode(list)) {
13 const nodeChildList = node.getChildren().filter(n => $isListNode(n))[0] || null;
14 const nodeChildItems = nodeChildList?.getChildren() || [];
16 const listItems = list.getChildren() as ListItemNode[];
17 const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
18 const isFirst = nodeIndex === 0;
20 const newListItem = $createListItemNode();
21 const newList = $createListNode(list.getListType());
22 newList.append(newListItem);
23 newListItem.append(...node.getChildren());
28 const prevListItem = listItems[nodeIndex - 1];
29 prevListItem.append(newList);
34 for (const child of nodeChildItems) {
35 newListItem.insertAfter(child);
37 nodeChildList.remove();
43 export function $unnestListItem(node: ListItemNode): ListItemNode {
44 const list = node.getParent();
45 const parentListItem = list?.getParent();
46 const outerList = parentListItem?.getParent();
47 if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
51 const laterSiblings = node.getNextSiblings();
52 parentListItem.insertAfter(node);
53 if (list.getChildren().length === 0) {
57 if (laterSiblings.length > 0) {
58 const childList = $createListNode(list.getListType());
59 childList.append(...laterSiblings);
60 node.append(childList);
63 if (list.getChildrenSize() === 0) {
67 if (parentListItem.getChildren().length === 0) {
68 parentListItem.remove();
74 function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
75 const nodes = selection?.getNodes() || [];
76 let [start, end] = selection?.getStartEndPoints() || [null, null];
78 // Ensure we ignore parent list items of the top-most list item since,
79 // although technically part of the selection, from a user point of
80 // view the selection does not spread to encompass this outer element.
81 const itemsToIgnore: Set<string> = new Set();
82 if (selection && start) {
83 if (selection.isBackward() && end) {
84 [end, start] = [start, end];
87 const startParents = start.getNode().getParents();
88 let foundList = false;
89 for (const parent of startParents) {
90 if ($isListItemNode(parent)) {
92 itemsToIgnore.add(parent.getKey());
100 const listItemNodes = [];
101 outer: for (const node of nodes) {
102 if ($isListItemNode(node)) {
103 if (!itemsToIgnore.has(node.getKey())) {
104 listItemNodes.push(node);
109 const parents = node.getParents();
110 for (const parent of parents) {
111 if ($isListItemNode(parent)) {
112 if (!itemsToIgnore.has(parent.getKey())) {
113 listItemNodes.push(parent);
119 listItemNodes.push(null);
122 return listItemNodes;
125 function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
126 const listItemMap: Record<string, ListItemNode> = {};
128 for (const item of listItems) {
133 const key = item.getKey();
134 if (typeof listItemMap[key] === 'undefined') {
135 listItemMap[key] = item;
139 const items = Object.values(listItemMap);
140 return $sortNodes(items) as ListItemNode[];
143 export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
144 const selection = $getSelection();
145 const selectionBounds = selection?.getStartEndPoints();
146 const listItemsInSelection = getListItemsForSelection(selection);
147 const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);
149 if (isListSelection) {
150 const alteredListItems = [];
151 const listItems = $reduceDedupeListItems(listItemsInSelection);
153 for (const listItem of listItems) {
154 alteredListItems.push($nestListItem(listItem));
156 } else if (change < 0) {
157 for (const listItem of [...listItems].reverse()) {
158 alteredListItems.push($unnestListItem(listItem));
160 alteredListItems.reverse();
163 if (alteredListItems.length === 1 && selectionBounds) {
164 // Retain selection range if moving just one item
165 const listItem = alteredListItems[0] as ListItemNode;
166 let child = listItem.getChildren()[0] as TextNode;
168 child = $createTextNode('');
169 listItem.append(child);
171 child.select(selectionBounds[0].offset, selectionBounds[1].offset);
173 $selectNodes(alteredListItems);
179 const elements = $getBlockElementNodesInSelection(selection);
180 for (const node of elements) {
181 if (nodeHasInset(node)) {
182 const currentInset = node.getInset();
183 const newInset = Math.min(Math.max(currentInset + change, 0), 500);
184 node.setInset(newInset)
188 $toggleSelection(editor);