]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts
Lexical: Made summary part of details node
[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
22     static getType() {
23         return 'details';
24     }
25
26     setId(id: string) {
27         const self = this.getWritable();
28         self.__id = id;
29     }
30
31     getId(): string {
32         const self = this.getLatest();
33         return self.__id;
34     }
35
36     setSummary(summary: string) {
37         const self = this.getWritable();
38         self.__summary = summary;
39     }
40
41     getSummary(): string {
42         const self = this.getLatest();
43         return self.__summary;
44     }
45
46     static clone(node: DetailsNode): DetailsNode {
47         const newNode =  new DetailsNode(node.__key);
48         newNode.__id = node.__id;
49         newNode.__dir = node.__dir;
50         newNode.__summary = node.__summary;
51         return newNode;
52     }
53
54     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
55         const el = document.createElement('details');
56         if (this.__id) {
57             el.setAttribute('id', this.__id);
58         }
59
60         if (this.__dir) {
61             el.setAttribute('dir', this.__dir);
62         }
63
64         const summary = document.createElement('summary');
65         summary.textContent = this.__summary;
66         summary.setAttribute('contenteditable', 'false');
67         el.append(summary);
68
69         return el;
70     }
71
72     updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
73         return prevNode.__id !== this.__id
74         || prevNode.__dir !== this.__dir;
75     }
76
77     static importDOM(): DOMConversionMap|null {
78         return {
79             details(node: HTMLElement): DOMConversion|null {
80                 return {
81                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
82                         const node = new DetailsNode();
83                         if (element.id) {
84                             node.setId(element.id);
85                         }
86
87                         if (element.dir) {
88                             node.setDirection(extractDirectionFromElement(element));
89                         }
90
91                         const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');
92                         node.setSummary(summaryElem?.textContent || '');
93
94                         return {node};
95                     },
96                     priority: 3,
97                 };
98             },
99             summary(node: HTMLElement): DOMConversion|null {
100                 return {
101                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
102                         return {node: 'ignore'};
103                     },
104                     priority: 3,
105                 };
106             },
107         };
108     }
109
110     exportDOM(editor: LexicalEditor): DOMExportOutput {
111         const element = this.createDOM(editor._config, editor);
112         const editable = element.querySelectorAll('[contenteditable]');
113         for (const elem of editable) {
114             elem.removeAttribute('contenteditable');
115         }
116
117         return {element};
118     }
119
120     exportJSON(): SerializedDetailsNode {
121         return {
122             ...super.exportJSON(),
123             type: 'details',
124             version: 1,
125             id: this.__id,
126             summary: this.__summary,
127         };
128     }
129
130     static importJSON(serializedNode: SerializedDetailsNode): DetailsNode {
131         const node = $createDetailsNode();
132         node.setId(serializedNode.id);
133         node.setDirection(serializedNode.direction);
134         return node;
135     }
136
137 }
138
139 export function $createDetailsNode() {
140     return new DetailsNode();
141 }
142
143 export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
144     return node instanceof DetailsNode;
145 }