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