]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts
Updated translator & dependency attribution before release v25.05.2
[bookstack] / resources / js / wysiwyg / lexical / rich-text / LexicalDetailsNode.ts
1 import {
2     DOMConversion,
3     DOMConversionMap, DOMConversionOutput,
4     ElementNode,
5     LexicalEditor,
6     LexicalNode,
7     SerializedElementNode, Spread,
8     EditorConfig, DOMExportOutput,
9 } from 'lexical';
10
11 import {extractDirectionFromElement} from "lexical/nodes/common";
12
13 export type SerializedDetailsNode = Spread<{
14     id: string;
15     summary: string;
16 }, SerializedElementNode>
17
18 export class DetailsNode extends ElementNode {
19     __id: string = '';
20     __summary: string = '';
21     __open: boolean = false;
22
23     static getType() {
24         return 'details';
25     }
26
27     setId(id: string) {
28         const self = this.getWritable();
29         self.__id = id;
30     }
31
32     getId(): string {
33         const self = this.getLatest();
34         return self.__id;
35     }
36
37     setSummary(summary: string) {
38         const self = this.getWritable();
39         self.__summary = summary;
40     }
41
42     getSummary(): string {
43         const self = this.getLatest();
44         return self.__summary;
45     }
46
47     setOpen(open: boolean) {
48         const self = this.getWritable();
49         self.__open = open;
50     }
51
52     getOpen(): boolean {
53         const self = this.getLatest();
54         return self.__open;
55     }
56
57     static clone(node: DetailsNode): DetailsNode {
58         const newNode =  new DetailsNode(node.__key);
59         newNode.__id = node.__id;
60         newNode.__dir = node.__dir;
61         newNode.__summary = node.__summary;
62         newNode.__open = node.__open;
63         return newNode;
64     }
65
66     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
67         const el = document.createElement('details');
68         if (this.__id) {
69             el.setAttribute('id', this.__id);
70         }
71
72         if (this.__dir) {
73             el.setAttribute('dir', this.__dir);
74         }
75
76         if (this.__open) {
77             el.setAttribute('open', 'true');
78         }
79
80         const summary = document.createElement('summary');
81         summary.textContent = this.__summary;
82         summary.setAttribute('contenteditable', 'false');
83         summary.addEventListener('click', event => {
84             event.preventDefault();
85             _editor.update(() => {
86                 this.select();
87             })
88         });
89
90         el.append(summary);
91
92         return el;
93     }
94
95     updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
96
97         if (prevNode.__open !== this.__open) {
98             dom.toggleAttribute('open', this.__open);
99         }
100
101         return prevNode.__id !== this.__id
102         || prevNode.__dir !== this.__dir
103         || prevNode.__summary !== this.__summary;
104     }
105
106     static importDOM(): DOMConversionMap|null {
107         return {
108             details(node: HTMLElement): DOMConversion|null {
109                 return {
110                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
111                         const node = new DetailsNode();
112                         if (element.id) {
113                             node.setId(element.id);
114                         }
115
116                         if (element.dir) {
117                             node.setDirection(extractDirectionFromElement(element));
118                         }
119
120                         const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');
121                         node.setSummary(summaryElem?.textContent || '');
122
123                         return {node};
124                     },
125                     priority: 3,
126                 };
127             },
128             summary(node: HTMLElement): DOMConversion|null {
129                 return {
130                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
131                         return {node: 'ignore'};
132                     },
133                     priority: 3,
134                 };
135             },
136         };
137     }
138
139     exportDOM(editor: LexicalEditor): DOMExportOutput {
140         const element = this.createDOM(editor._config, editor);
141         const editable = element.querySelectorAll('[contenteditable]');
142         for (const elem of editable) {
143             elem.removeAttribute('contenteditable');
144         }
145
146         element.removeAttribute('open');
147
148         return {element};
149     }
150
151     exportJSON(): SerializedDetailsNode {
152         return {
153             ...super.exportJSON(),
154             type: 'details',
155             version: 1,
156             id: this.__id,
157             summary: this.__summary,
158         };
159     }
160
161     static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {
162         const node = $createDetailsNode();
163         node.setId(serializedNode.id);
164         node.setDirection(serializedNode.direction);
165         return node;
166     }
167
168 }
169
170 export function $createDetailsNode() {
171     return new DetailsNode();
172 }
173
174 export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
175     return node instanceof DetailsNode;
176 }