]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/code-block.ts
Lexical: Started diagram support
[bookstack] / resources / js / wysiwyg / nodes / code-block.ts
1 import {
2     DecoratorNode,
3     DOMConversion,
4     DOMConversionMap,
5     DOMConversionOutput,
6     LexicalEditor, LexicalNode,
7     SerializedLexicalNode,
8     Spread
9 } from "lexical";
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import {el} from "../helpers";
12 import {EditorDecoratorAdapter} from "../ui/framework/decorator";
13
14 export type SerializedCodeBlockNode = Spread<{
15     language: string;
16     id: string;
17     code: string;
18 }, SerializedLexicalNode>
19
20 const getLanguageFromClassList = (classes: string) => {
21     const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
22     return (langClasses[0] || '').replace('language-', '');
23 };
24
25 export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
26     __id: string = '';
27     __language: string = '';
28     __code: string = '';
29
30     static getType(): string {
31         return 'code-block';
32     }
33
34     static clone(node: CodeBlockNode): CodeBlockNode {
35         return new CodeBlockNode(node.__language, node.__code);
36     }
37
38     constructor(language: string = '', code: string = '', key?: string) {
39         super(key);
40         this.__language = language;
41         this.__code = code;
42     }
43
44     setLanguage(language: string): void {
45         const self = this.getWritable();
46         self.__language = language;
47     }
48
49     getLanguage(): string {
50         const self = this.getLatest();
51         return self.__language;
52     }
53
54     setCode(code: string): void {
55         const self = this.getWritable();
56         self.__code = code;
57     }
58
59     getCode(): string {
60         const self = this.getLatest();
61         return self.__code;
62     }
63
64     setId(id: string) {
65         const self = this.getWritable();
66         self.__id = id;
67     }
68
69     getId(): string {
70         const self = this.getLatest();
71         return self.__id;
72     }
73
74     decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
75         return {
76             type: 'code',
77             getNode: () => this,
78         };
79     }
80
81     isInline(): boolean {
82         return false;
83     }
84
85     isIsolated() {
86         return true;
87     }
88
89     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
90         const codeBlock = el('pre', {
91             id: this.__id || null,
92         }, [
93             el('code', {
94                 class: this.__language ? `language-${this.__language}` : null,
95             }, [this.__code]),
96         ]);
97
98         return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
99     }
100
101     updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
102         const code = dom.querySelector('code');
103         if (!code) return false;
104
105         if (prevNode.__language !== this.__language) {
106             code.className = this.__language ? `language-${this.__language}` : '';
107         }
108
109         if (prevNode.__id !== this.__id) {
110             dom.setAttribute('id', this.__id);
111         }
112
113         if (prevNode.__code !== this.__code) {
114             code.textContent = this.__code;
115         }
116
117         return false;
118     }
119
120     static importDOM(): DOMConversionMap|null {
121         return {
122             pre(node: HTMLElement): DOMConversion|null {
123                 return {
124                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
125
126                         const codeEl = element.querySelector('code');
127                         const language = getLanguageFromClassList(element.className)
128                                         || (codeEl && getLanguageFromClassList(codeEl.className))
129                                         || '';
130
131                         const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
132
133                         return {
134                             node: $createCodeBlockNode(language, code),
135                         };
136                     },
137                     priority: 3,
138                 };
139             },
140         };
141     }
142
143     exportJSON(): SerializedCodeBlockNode {
144         return {
145             type: 'code-block',
146             version: 1,
147             id: this.__id,
148             language: this.__language,
149             code: this.__code,
150         };
151     }
152
153     static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
154         const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
155         node.setId(serializedNode.id || '');
156         return node;
157     }
158 }
159
160 export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
161     return new CodeBlockNode(language, code);
162 }
163
164 export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
165     return node instanceof CodeBlockNode;
166 }
167
168 export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
169     const code = node.getCode();
170     const language = node.getLanguage();
171
172     // @ts-ignore
173     const codeEditor = window.$components.first('code-editor');
174     // TODO - Handle direction
175     codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
176         editor.update(() => {
177             node.setCode(newCode);
178             node.setLanguage(newLang);
179         });
180         // TODO - Re-focus
181     }, () => {
182         // TODO - Re-focus
183     });
184 }