]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-list-item.ts
659a55a157a6fd9d129cc03992f6003b7662dcfa
[bookstack] / resources / js / wysiwyg / nodes / custom-list-item.ts
1 import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
2 import {EditorConfig} from "lexical/LexicalEditor";
3 import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
4
5 import {el} from "../utils/dom";
6 import {$isCustomListNode} from "./custom-list";
7
8 function updateListItemChecked(
9     dom: HTMLElement,
10     listItemNode: ListItemNode,
11 ): void {
12     // Only set task list attrs for leaf list items
13     const shouldBeTaskItem = !$isListNode(listItemNode.getFirstChild());
14     dom.classList.toggle('task-list-item', shouldBeTaskItem);
15     if (listItemNode.__checked) {
16         dom.setAttribute('checked', 'checked');
17     } else {
18         dom.removeAttribute('checked');
19     }
20 }
21
22
23 export class CustomListItemNode extends ListItemNode {
24     static getType(): string {
25         return 'custom-list-item';
26     }
27
28     static clone(node: CustomListItemNode): CustomListItemNode {
29         return new CustomListItemNode(node.__value, node.__checked, node.__key);
30     }
31
32     createDOM(config: EditorConfig): HTMLElement {
33         const element = document.createElement('li');
34         const parent = this.getParent();
35
36         if ($isListNode(parent) && parent.getListType() === 'check') {
37             updateListItemChecked(element, this);
38         }
39
40         element.value = this.__value;
41
42         if ($hasNestedListWithoutLabel(this)) {
43             element.style.listStyle = 'none';
44         }
45
46         return element;
47     }
48
49     updateDOM(
50         prevNode: ListItemNode,
51         dom: HTMLElement,
52         config: EditorConfig,
53     ): boolean {
54         const parent = this.getParent();
55         if ($isListNode(parent) && parent.getListType() === 'check') {
56             updateListItemChecked(dom, this);
57         }
58         // @ts-expect-error - this is always HTMLListItemElement
59         dom.value = this.__value;
60
61         return false;
62     }
63
64     exportDOM(editor: LexicalEditor): DOMExportOutput {
65         const element = this.createDOM(editor._config);
66         element.style.textAlign = this.getFormatType();
67
68         if (element.classList.contains('task-list-item')) {
69             const input = el('input', {
70                 type: 'checkbox',
71                 disabled: 'disabled',
72             });
73             if (element.hasAttribute('checked')) {
74                 input.setAttribute('checked', 'checked');
75                 element.removeAttribute('checked');
76             }
77
78             element.prepend(input);
79         }
80
81         return {
82             element,
83         };
84     }
85
86     exportJSON(): SerializedListItemNode {
87         return {
88             ...super.exportJSON(),
89             type: 'custom-list-item',
90         };
91     }
92 }
93
94 function $hasNestedListWithoutLabel(node: CustomListItemNode): boolean {
95     const children = node.getChildren();
96     let hasLabel = false;
97     let hasNestedList = false;
98
99     for (const child of children) {
100         if ($isCustomListNode(child)) {
101             hasNestedList = true;
102         } else if (child.getTextContent().trim().length > 0) {
103             hasLabel = true;
104         }
105     }
106
107     return hasNestedList && !hasLabel;
108 }
109
110 export function $isCustomListItemNode(
111     node: LexicalNode | null | undefined,
112 ): node is CustomListItemNode {
113     return node instanceof CustomListItemNode;
114 }
115
116 export function $createCustomListItemNode(): CustomListItemNode {
117     return new CustomListItemNode();
118 }