]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/lists.ts
Lexical: Merged list nodes
[bookstack] / resources / js / wysiwyg / utils / lists.ts
1 import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
2 import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
3 import {nodeHasInset} from "./nodes";
4 import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list";
5
6
7 export function $nestListItem(node: ListItemNode): ListItemNode {
8     const list = node.getParent();
9     if (!$isListNode(list)) {
10         return node;
11     }
12
13     const listItems = list.getChildren() as ListItemNode[];
14     const nodeIndex = listItems.findIndex((n) => n.getKey() === node.getKey());
15     const isFirst = nodeIndex === 0;
16
17     const newListItem = $createListItemNode();
18     const newList = $createListNode(list.getListType());
19     newList.append(newListItem);
20     newListItem.append(...node.getChildren());
21
22     if (isFirst) {
23         node.append(newList);
24     } else  {
25         const prevListItem = listItems[nodeIndex - 1];
26         prevListItem.append(newList);
27         node.remove();
28     }
29
30     return newListItem;
31 }
32
33 export function $unnestListItem(node: ListItemNode): ListItemNode {
34     const list = node.getParent();
35     const parentListItem = list?.getParent();
36     const outerList = parentListItem?.getParent();
37     if (!$isListNode(list) || !$isListNode(outerList) || !$isListItemNode(parentListItem)) {
38         return node;
39     }
40
41     parentListItem.insertAfter(node);
42     if (list.getChildren().length === 0) {
43         list.remove();
44     }
45
46     if (parentListItem.getChildren().length === 0) {
47         parentListItem.remove();
48     }
49
50     return node;
51 }
52
53 function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] {
54     const nodes = selection?.getNodes() || [];
55     const listItemNodes = [];
56
57     outer: for (const node of nodes) {
58         if ($isListItemNode(node)) {
59             listItemNodes.push(node);
60             continue;
61         }
62
63         const parents = node.getParents();
64         for (const parent of parents) {
65             if ($isListItemNode(parent)) {
66                 listItemNodes.push(parent);
67                 continue outer;
68             }
69         }
70
71         listItemNodes.push(null);
72     }
73
74     return listItemNodes;
75 }
76
77 function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[] {
78     const listItemMap: Record<string, ListItemNode> = {};
79
80     for (const item of listItems) {
81         if (item === null) {
82             continue;
83         }
84
85         const key = item.getKey();
86         if (typeof listItemMap[key] === 'undefined') {
87             listItemMap[key] = item;
88         }
89     }
90
91     return Object.values(listItemMap);
92 }
93
94 export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
95     const selection = $getSelection();
96     const listItemsInSelection = getListItemsForSelection(selection);
97     const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);
98
99     if (isListSelection) {
100         const alteredListItems = [];
101         const listItems = $reduceDedupeListItems(listItemsInSelection);
102         if (change > 0) {
103             for (const listItem of listItems) {
104                 alteredListItems.push($nestListItem(listItem));
105             }
106         } else if (change < 0) {
107             for (const listItem of [...listItems].reverse()) {
108                 alteredListItems.push($unnestListItem(listItem));
109             }
110             alteredListItems.reverse();
111         }
112
113         $selectNodes(alteredListItems);
114         return;
115     }
116
117     const elements = $getBlockElementNodesInSelection(selection);
118     for (const node of elements) {
119         if (nodeHasInset(node)) {
120             const currentInset = node.getInset();
121             const newInset = Math.min(Math.max(currentInset + change, 0), 500);
122             node.setInset(newInset)
123         }
124     }
125
126     $toggleSelection(editor);
127 }