]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started code block node implementation
authorDan Brown <redacted>
Tue, 2 Jul 2024 13:46:30 +0000 (14:46 +0100)
committerDan Brown <redacted>
Tue, 2 Jul 2024 13:46:30 +0000 (14:46 +0100)
resources/js/wysiwyg/nodes/code-block.ts [new file with mode: 0644]
resources/js/wysiwyg/nodes/index.ts
resources/js/wysiwyg/ui/decorators/code-block.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/framework/decorator.ts
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/index.ts

diff --git a/resources/js/wysiwyg/nodes/code-block.ts b/resources/js/wysiwyg/nodes/code-block.ts
new file mode 100644 (file)
index 0000000..7184334
--- /dev/null
@@ -0,0 +1,168 @@
+import {
+    DecoratorNode,
+    DOMConversion,
+    DOMConversionMap,
+    DOMConversionOutput,
+    LexicalEditor, LexicalNode,
+    SerializedLexicalNode,
+    Spread
+} from "lexical";
+import type {EditorConfig} from "lexical/LexicalEditor";
+import {el} from "../helpers";
+import {EditorDecoratorAdapter} from "../ui/framework/decorator";
+import {code} from "../ui/defaults/button-definitions";
+
+export type SerializedCodeBlockNode = Spread<{
+    language: string;
+    id: string;
+    code: string;
+}, SerializedLexicalNode>
+
+const getLanguageFromClassList = (classes: string) => {
+    const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
+    return (langClasses[0] || '').replace('language-', '');
+};
+
+export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
+    __id: string = '';
+    __language: string = '';
+    __code: string = '';
+
+    static getType(): string {
+        return 'code-block';
+    }
+
+    static clone(node: CodeBlockNode): CodeBlockNode {
+        return new CodeBlockNode(node.__language, node.__code);
+    }
+
+    constructor(language: string = '', code: string = '', key?: string) {
+        super(key);
+        this.__language = language;
+        this.__code = code;
+    }
+
+    setLanguage(language: string): void {
+        const self = this.getWritable();
+        self.__language = language;
+    }
+
+    getLanguage(): string {
+        const self = this.getLatest();
+        return self.__language;
+    }
+
+    setCode(code: string): void {
+        const self = this.getWritable();
+        self.__code = code;
+    }
+
+    getCode(): string {
+        const self = this.getLatest();
+        return self.__code;
+    }
+
+    setId(id: string) {
+        const self = this.getWritable();
+        self.__id = id;
+    }
+
+    getId(): string {
+        const self = this.getLatest();
+        return self.__id;
+    }
+
+    decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
+        // TODO
+        return {
+            type: 'code',
+            getNode: () => this,
+        };
+    }
+
+    isInline(): boolean {
+        return false;
+    }
+
+    isIsolated() {
+        return true;
+    }
+
+    createDOM(_config: EditorConfig, _editor: LexicalEditor) {
+        const codeBlock = el('pre', {
+            id: this.__id || null,
+        }, [
+            el('code', {
+                class: this.__language ? `language-${this.__language}` : null,
+            }, [this.__code]),
+        ]);
+
+        return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
+    }
+
+    updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
+        const code = dom.querySelector('code');
+        if (!code) return false;
+
+        if (prevNode.__language !== this.__language) {
+            code.className = this.__language ? `language-${this.__language}` : '';
+        }
+
+        if (prevNode.__id !== this.__id) {
+            dom.setAttribute('id', this.__id);
+        }
+
+        if (prevNode.__code !== this.__code) {
+            code.textContent = this.__code;
+        }
+
+        return false;
+    }
+
+    static importDOM(): DOMConversionMap|null {
+        return {
+            pre(node: HTMLElement): DOMConversion|null {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+
+                        const codeEl = element.querySelector('code');
+                        const language = getLanguageFromClassList(element.className)
+                                        || (codeEl && getLanguageFromClassList(codeEl.className))
+                                        || '';
+
+                        const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
+
+                        return {
+                            node: $createCodeBlockNode(language, code),
+                        };
+                    },
+                    priority: 3,
+                };
+            },
+        };
+    }
+
+    exportJSON(): SerializedCodeBlockNode {
+        return {
+            type: 'code-block',
+            version: 1,
+            id: this.__id,
+            language: this.__language,
+            code: this.__code,
+        };
+    }
+
+    static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
+        const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
+        node.setId(serializedNode.id || '');
+        return node;
+    }
+}
+
+export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
+    return new CodeBlockNode(language, code);
+}
+
+export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
+    return node instanceof CodeBlockNode;
+}
\ No newline at end of file
index befc2ab2e05442f5f1ef49d2edc8030252443428..4cc6bd08b784e831386be71daddc3524b381272d 100644 (file)
@@ -9,6 +9,7 @@ import {ListItemNode, ListNode} from "@lexical/list";
 import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
 import {CustomTableNode} from "./custom-table";
 import {HorizontalRuleNode} from "./horizontal-rule";
+import {CodeBlockNode} from "./code-block";
 
 /**
  * Load the nodes for lexical.
@@ -26,6 +27,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         ImageNode,
         HorizontalRuleNode,
         DetailsNode, SummaryNode,
+        CodeBlockNode,
         CustomParagraphNode,
         LinkNode,
         {
diff --git a/resources/js/wysiwyg/ui/decorators/code-block.ts b/resources/js/wysiwyg/ui/decorators/code-block.ts
new file mode 100644 (file)
index 0000000..f1fd8c1
--- /dev/null
@@ -0,0 +1,42 @@
+import {EditorDecorator} from "../framework/decorator";
+import {el} from "../../helpers";
+import {EditorUiContext} from "../framework/core";
+import {CodeBlockNode} from "../../nodes/code-block";
+
+
+export class CodeBlockDecorator extends EditorDecorator {
+
+     render(context: EditorUiContext, element: HTMLElement): void {
+        const codeNode = this.getNode() as CodeBlockNode;
+        const preEl = element.querySelector('pre');
+        if (preEl) {
+            preEl.hidden = true;
+        }
+
+        const code = codeNode.__code;
+        const language = codeNode.__language;
+        const lines = code.split('\n').length;
+        const height = (lines * 19.2) + 18 + 24;
+        element.style.height = `${height}px`;
+
+        let editor = null;
+        const startTime = Date.now();
+
+        // Todo - Handling click/edit control
+         // Todo - Add toolbar button for code
+
+        // @ts-ignore
+        const renderEditor = (Code) => {
+            editor = Code.wysiwygView(element, document, code, language);
+            setTimeout(() => {
+                element.style.height = '';
+            }, 12);
+        };
+
+        // @ts-ignore
+        window.importVersioned('code').then((Code) => {
+            const timeout = (Date.now() - startTime < 20) ? 20 : 0;
+            setTimeout(() => renderEditor(Code), timeout);
+        });
+    }
+}
\ No newline at end of file
index 89077412691154861388e5e856247d19bddaef6b..b0d2392fd31d4a62d78dd95d1586befc14bbf6f3 100644 (file)
@@ -27,6 +27,11 @@ export abstract class EditorDecorator {
         this.node = node;
     }
 
-    abstract render(context: EditorUiContext): HTMLElement;
+    /**
+     * Render the decorator.
+     * If an element is returned, this will be appended to the element
+     * that is being decorated.
+     */
+    abstract render(context: EditorUiContext, decorated: HTMLElement): HTMLElement|void;
 
 }
\ No newline at end of file
index 3c2ad89265ca0390550bb7bb42f593ce4eb92ad6..a75d2478624434e25db4042182eb0ff5da1d035e 100644 (file)
@@ -160,11 +160,15 @@ export class EditorUIManager {
             const keys = Object.keys(decorators);
             for (const key of keys) {
                 const decoratedEl = editor.getElementByKey(key);
+                if (!decoratedEl) {
+                    continue;
+                }
+
                 const adapter = decorators[key];
                 const decorator = this.getDecorator(adapter.type, key);
                 decorator.setNode(adapter.getNode());
-                const decoratorEl = decorator.render(this.getContext());
-                if (decoratedEl) {
+                const decoratorEl = decorator.render(this.getContext(), decoratedEl);
+                if (decoratorEl) {
                     decoratedEl.append(decoratorEl);
                 }
             }
index 3501ed557b66da0a11bce7c71ec53182e562df4e..1ad1395dc7781289d449ba294dfbf96fe7779f61 100644 (file)
@@ -4,6 +4,7 @@ import {EditorUIManager} from "./framework/manager";
 import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
 import {ImageDecorator} from "./decorators/image";
 import {EditorUiContext} from "./framework/core";
+import {CodeBlockDecorator} from "./decorators/code-block";
 
 export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor) {
     const manager = new EditorUIManager();
@@ -49,4 +50,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
 
     // Register image decorator listener
     manager.registerDecoratorType('image', ImageDecorator);
+    manager.registerDecoratorType('code', CodeBlockDecorator);
 }
\ No newline at end of file