]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-list-item.ts
Lexical: Extracted & merged heading & quote nodes
[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
59         dom.style.listStyle = $hasNestedListWithoutLabel(this) ? 'none' : '';
60         // @ts-expect-error - this is always HTMLListItemElement
61         dom.value = this.__value;
62
63         return false;
64     }
65
66     exportDOM(editor: LexicalEditor): DOMExportOutput {
67         const element = this.createDOM(editor._config);
68         element.style.textAlign = this.getFormatType();
69
70         if (element.classList.contains('task-list-item')) {
71             const input = el('input', {
72                 type: 'checkbox',
73                 disabled: 'disabled',
74             });
75             if (element.hasAttribute('checked')) {
76                 input.setAttribute('checked', 'checked');
77                 element.removeAttribute('checked');
78             }
79
80             element.prepend(input);
81         }
82
83         return {
84             element,
85         };
86     }
87
88     exportJSON(): SerializedListItemNode {
89         return {
90             ...super.exportJSON(),
91             type: 'custom-list-item',
92         };
93     }
94 }
95
96 function $hasNestedListWithoutLabel(node: CustomListItemNode): boolean {
97     const children = node.getChildren();
98     let hasLabel = false;
99     let hasNestedList = false;
100
101     for (const child of children) {
102         if ($isCustomListNode(child)) {
103             hasNestedList = true;
104         } else if (child.getTextContent().trim().length > 0) {
105             hasLabel = true;
106         }
107     }
108
109     return hasNestedList && !hasLabel;
110 }
111
112 export function $isCustomListItemNode(
113     node: LexicalNode | null | undefined,
114 ): node is CustomListItemNode {
115     return node instanceof CustomListItemNode;
116 }
117
118 export function $createCustomListItemNode(): CustomListItemNode {
119     return new CustomListItemNode();
120 }