]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/lists.ts
WYSIWYG: Fixed list indenting selection & display bugs
[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 {$getSelection, BaseSelection, LexicalEditor} from "lexical";
4 import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection";
5 import {nodeHasInset} from "./nodes";
6
7
8 export function $nestListItem(node: CustomListItemNode): CustomListItemNode {
9     const list = node.getParent();
10     if (!$isCustomListNode(list)) {
11         return node;
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     return newListItem;
32 }
33
34 export function $unnestListItem(node: CustomListItemNode): CustomListItemNode {
35     const list = node.getParent();
36     const parentListItem = list?.getParent();
37     const outerList = parentListItem?.getParent();
38     if (!$isCustomListNode(list) || !$isCustomListNode(outerList) || !$isCustomListItemNode(parentListItem)) {
39         return node;
40     }
41
42     parentListItem.insertAfter(node);
43     if (list.getChildren().length === 0) {
44         list.remove();
45     }
46
47     if (parentListItem.getChildren().length === 0) {
48         parentListItem.remove();
49     }
50
51     return node;
52 }
53
54 function getListItemsForSelection(selection: BaseSelection|null): (CustomListItemNode|null)[] {
55     const nodes = selection?.getNodes() || [];
56     const listItemNodes = [];
57
58     outer: for (const node of nodes) {
59         if ($isCustomListItemNode(node)) {
60             listItemNodes.push(node);
61             continue;
62         }
63
64         const parents = node.getParents();
65         for (const parent of parents) {
66             if ($isCustomListItemNode(parent)) {
67                 listItemNodes.push(parent);
68                 continue outer;
69             }
70         }
71
72         listItemNodes.push(null);
73     }
74
75     return listItemNodes;
76 }
77
78 function $reduceDedupeListItems(listItems: (CustomListItemNode|null)[]): CustomListItemNode[] {
79     const listItemMap: Record<string, CustomListItemNode> = {};
80
81     for (const item of listItems) {
82         if (item === null) {
83             continue;
84         }
85
86         const key = item.getKey();
87         if (typeof listItemMap[key] === 'undefined') {
88             listItemMap[key] = item;
89         }
90     }
91
92     return Object.values(listItemMap);
93 }
94
95 export function $setInsetForSelection(editor: LexicalEditor, change: number): void {
96     const selection = $getSelection();
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         $selectNodes(alteredListItems);
115         return;
116     }
117
118     const elements = $getBlockElementNodesInSelection(selection);
119     for (const node of elements) {
120         if (nodeHasInset(node)) {
121             const currentInset = node.getInset();
122             const newInset = Math.min(Math.max(currentInset + change, 0), 500);
123             node.setInset(newInset)
124         }
125     }
126
127     $toggleSelection(editor);
128 }