]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/code-block.ts
JS: Converted come common services to typescript
[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 {CodeEditor} from "../../components";
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         return {
77             type: 'code',
78             getNode: () => this,
79         };
80     }
81
82     isInline(): boolean {
83         return false;
84     }
85
86     isIsolated() {
87         return true;
88     }
89
90     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
91         const codeBlock = el('pre', {
92             id: this.__id || null,
93         }, [
94             el('code', {
95                 class: this.__language ? `language-${this.__language}` : null,
96             }, [this.__code]),
97         ]);
98
99         return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
100     }
101
102     updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
103         const code = dom.querySelector('code');
104         if (!code) return false;
105
106         if (prevNode.__language !== this.__language) {
107             code.className = this.__language ? `language-${this.__language}` : '';
108         }
109
110         if (prevNode.__id !== this.__id) {
111             dom.setAttribute('id', this.__id);
112         }
113
114         if (prevNode.__code !== this.__code) {
115             code.textContent = this.__code;
116         }
117
118         return false;
119     }
120
121     static importDOM(): DOMConversionMap|null {
122         return {
123             pre(node: HTMLElement): DOMConversion|null {
124                 return {
125                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
126
127                         const codeEl = element.querySelector('code');
128                         const language = getLanguageFromClassList(element.className)
129                                         || (codeEl && getLanguageFromClassList(codeEl.className))
130                                         || '';
131
132                         const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
133
134                         return {
135                             node: $createCodeBlockNode(language, code),
136                         };
137                     },
138                     priority: 3,
139                 };
140             },
141         };
142     }
143
144     exportJSON(): SerializedCodeBlockNode {
145         return {
146             type: 'code-block',
147             version: 1,
148             id: this.__id,
149             language: this.__language,
150             code: this.__code,
151         };
152     }
153
154     static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
155         const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
156         node.setId(serializedNode.id || '');
157         return node;
158     }
159 }
160
161 export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
162     return new CodeBlockNode(language, code);
163 }
164
165 export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
166     return node instanceof CodeBlockNode;
167 }
168
169 export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
170     const code = node.getCode();
171     const language = node.getLanguage();
172
173     // @ts-ignore
174     const codeEditor = window.$components.first('code-editor') as CodeEditor;
175     // TODO - Handle direction
176     codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
177         editor.update(() => {
178             node.setCode(newCode);
179             node.setLanguage(newLang);
180         });
181         // TODO - Re-focus
182     }, () => {
183         // TODO - Re-focus
184     });
185 }