]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/lists.ts
2fc1c5f6b4ed3d2a3ff14368017381e7513061d6
[bookstack] / resources / js / wysiwyg / utils / lists.ts
1 import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} 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 selectionBounds = selection?.getStartEndPoints();
97     const listItemsInSelection = getListItemsForSelection(selection);
98     const isListSelection = listItemsInSelection.length > 0 && !listItemsInSelection.includes(null);
99
100     if (isListSelection) {
101         const alteredListItems = [];
102         const listItems = $reduceDedupeListItems(listItemsInSelection);
103         if (change > 0) {
104             for (const listItem of listItems) {
105                 alteredListItems.push($nestListItem(listItem));
106             }
107         } else if (change < 0) {
108             for (const listItem of [...listItems].reverse()) {
109                 alteredListItems.push($unnestListItem(listItem));
110             }
111             alteredListItems.reverse();
112         }
113
114         if (alteredListItems.length === 1 && selectionBounds) {
115             // Retain selection range if moving just one item
116             const listItem = alteredListItems[0] as ListItemNode;
117             let child = listItem.getChildren()[0] as TextNode;
118             if (!child) {
119                 child = $createTextNode('');
120                 listItem.append(child);
121             }
122             child.select(selectionBounds[0].offset, selectionBounds[1].offset);
123         } else {
124             $selectNodes(alteredListItems);
125         }
126
127         return;
128     }
129
130     const elements = $getBlockElementNodesInSelection(selection);
131     for (const node of elements) {
132         if (nodeHasInset(node)) {
133             const currentInset = node.getInset();
134             const newInset = Math.min(Math.max(currentInset + change, 0), 500);
135             node.setInset(newInset)
136         }
137     }
138
139     $toggleSelection(editor);
140 }