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