]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Linked code block to editor, added button
authorDan Brown <redacted>
Tue, 2 Jul 2024 16:34:03 +0000 (17:34 +0100)
committerDan Brown <redacted>
Tue, 2 Jul 2024 16:34:03 +0000 (17:34 +0100)
resources/js/global.d.ts
resources/js/wysiwyg/nodes/code-block.ts
resources/js/wysiwyg/ui/decorators/code-block.ts
resources/js/wysiwyg/ui/defaults/button-definitions.ts
resources/js/wysiwyg/ui/framework/decorator.ts
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/toolbars.ts
resources/sass/_editor.scss

index c5aba8ee287986e88d99c9f74a4006d0a9914b0c..537da63685455d1f4cdca2bf5c088528948b0a73 100644 (file)
@@ -1,4 +1,12 @@
 declare module '*.svg' {
     const content: string;
     export default content;
+}
+
+declare global {
+    interface Window {
+        $components: {
+            first: (string) => Object,
+        }
+    }
 }
\ No newline at end of file
index 7184334a0d2c03ccdf50bf2cf94ca770b9c05c97..934fe7eddd695cf686b6e9953f1c702d89f9708e 100644 (file)
@@ -73,7 +73,6 @@ export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
     }
 
     decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
-        // TODO
         return {
             type: 'code',
             getNode: () => this,
@@ -165,4 +164,22 @@ export function $createCodeBlockNode(language: string = '', code: string = ''):
 
 export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
     return node instanceof CodeBlockNode;
+}
+
+export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
+    const code = node.getCode();
+    const language = node.getLanguage();
+
+    // @ts-ignore
+    const codeEditor = window.$components.first('code-editor');
+    // TODO - Handle direction
+    codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
+        editor.update(() => {
+            node.setCode(newCode);
+            node.setLanguage(newLang);
+        });
+        // TODO - Re-focus
+    }, () => {
+        // TODO - Re-focus
+    });
 }
\ No newline at end of file
index f1fd8c1994b2735e75de354f73f4100813e1532f..80dcef3bdeaa18aa6ae7f755338a9819089bcb3a 100644 (file)
@@ -1,33 +1,46 @@
 import {EditorDecorator} from "../framework/decorator";
-import {el} from "../../helpers";
 import {EditorUiContext} from "../framework/core";
-import {CodeBlockNode} from "../../nodes/code-block";
+import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
+import {ImageNode} from "../../nodes/image";
 
 
 export class CodeBlockDecorator extends EditorDecorator {
 
-     render(context: EditorUiContext, element: HTMLElement): void {
+    protected completedSetup: boolean = false;
+    protected latestCode: string = '';
+    protected latestLanguage: string = '';
+
+    // @ts-ignore
+    protected editor: any = null;
+
+    setup(context: EditorUiContext, element: HTMLElement) {
         const codeNode = this.getNode() as CodeBlockNode;
         const preEl = element.querySelector('pre');
+        if (!preEl) {
+            return;
+        }
+
         if (preEl) {
             preEl.hidden = true;
         }
 
-        const code = codeNode.__code;
-        const language = codeNode.__language;
-        const lines = code.split('\n').length;
+        this.latestCode = codeNode.__code;
+        this.latestLanguage = codeNode.__language;
+        const lines = this.latestCode.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
+        element.addEventListener('dblclick', event => {
+            context.editor.getEditorState().read(() => {
+                $openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode));
+            });
+        });
 
         // @ts-ignore
         const renderEditor = (Code) => {
-            editor = Code.wysiwygView(element, document, code, language);
+            this.editor = Code.wysiwygView(element, document, this.latestCode, this.latestLanguage);
             setTimeout(() => {
                 element.style.height = '';
             }, 12);
@@ -38,5 +51,32 @@ export class CodeBlockDecorator extends EditorDecorator {
             const timeout = (Date.now() - startTime < 20) ? 20 : 0;
             setTimeout(() => renderEditor(Code), timeout);
         });
+
+        this.completedSetup = true;
+    }
+
+    update() {
+        const codeNode = this.getNode() as CodeBlockNode;
+        const code = codeNode.getCode();
+        const language = codeNode.getLanguage();
+
+        if (this.latestCode === code && this.latestLanguage === language) {
+            return;
+        }
+        this.latestLanguage = language;
+        this.latestCode = code;
+
+        if (this.editor) {
+            this.editor.setContent(code);
+            this.editor.setMode(language, code);
+        }
+    }
+
+    render(context: EditorUiContext, element: HTMLElement): void {
+        if (this.completedSetup) {
+            this.update();
+        } else {
+            this.setup(context, element);
+        }
     }
 }
\ No newline at end of file
index 4a45ef75d0a75656a29929df45046b3b5fc51dc2..9f83fbea363a1c14bd9d3ad09eefb6e873e5727f 100644 (file)
@@ -49,10 +49,12 @@ import unlinkIcon from "@icons/editor/unlink.svg"
 import tableIcon from "@icons/editor/table.svg"
 import imageIcon from "@icons/editor/image.svg"
 import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
+import codeBlockIcon from "@icons/editor/code-block.svg"
 import detailsIcon from "@icons/editor/details.svg"
 import sourceIcon from "@icons/editor/source-view.svg"
 import fullscreenIcon from "@icons/editor/fullscreen.svg"
 import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
+import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
 
 export const undo: EditorButtonDefinition = {
     label: 'Undo',
@@ -336,6 +338,31 @@ export const horizontalRule: EditorButtonDefinition = {
     }
 };
 
+export const codeBlock: EditorButtonDefinition = {
+    label: 'Insert code block',
+    icon: codeBlockIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const codeBlock = getNodeFromSelection(selection, $isCodeBlockNode) as (CodeBlockNode|null);
+            if (codeBlock === null) {
+                context.editor.update(() => {
+                    const codeBlock = $createCodeBlockNode();
+                    codeBlock.setCode(selection?.getTextContent() || '');
+                    insertNewBlockNodeAtSelection(codeBlock, true);
+                    $openCodeEditorForNode(context.editor, codeBlock);
+                    codeBlock.selectStart();
+                });
+            } else {
+                $openCodeEditorForNode(context.editor, codeBlock);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return selectionContainsNodeType(selection, $isCodeBlockNode);
+    }
+};
+
 export const details: EditorButtonDefinition = {
     label: 'Insert collapsible block',
     icon: detailsIcon,
index b0d2392fd31d4a62d78dd95d1586befc14bbf6f3..a9917ab232fb0232f8ac15ce2a9e8dd8f7829a42 100644 (file)
@@ -29,6 +29,7 @@ export abstract class EditorDecorator {
 
     /**
      * Render the decorator.
+     * Can run on both creation and update for a node decorator.
      * If an element is returned, this will be appended to the element
      * that is being decorated.
      */
index a75d2478624434e25db4042182eb0ff5da1d035e..6477c4a1a76e5acf2bd2bf5be1093186921fbdaa 100644 (file)
@@ -157,21 +157,23 @@ export class EditorUIManager {
 
         // Register our DOM decorate listener with the editor
         const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => {
-            const keys = Object.keys(decorators);
-            for (const key of keys) {
-                const decoratedEl = editor.getElementByKey(key);
-                if (!decoratedEl) {
-                    continue;
+            editor.getEditorState().read(() => {
+                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(), decoratedEl);
+                    if (decoratorEl) {
+                        decoratedEl.append(decoratorEl);
+                    }
                 }
-
-                const adapter = decorators[key];
-                const decorator = this.getDecorator(adapter.type, key);
-                decorator.setNode(adapter.getNode());
-                const decoratorEl = decorator.render(this.getContext(), decoratedEl);
-                if (decoratorEl) {
-                    decoratedEl.append(decoratorEl);
-                }
-            }
+            });
         }
         editor.registerDecoratorListener(domDecorateListener);
     }
index 550c798c264e4636cc3a44cfb9096391db463bb3..18b81138007edd0d1b6db22de32503f85eeeaf70 100644 (file)
@@ -1,6 +1,6 @@
 import {EditorButton} from "./framework/buttons";
 import {
-    blockquote, bold, bulletList, clearFormating, code,
+    blockquote, bold, bulletList, clearFormating, code, codeBlock,
     dangerCallout, details, fullscreen,
     h2, h3, h4, h5, highlightColor, horizontalRule, image,
     infoCallout, italic, link, numberList, paragraph,
@@ -68,6 +68,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
             ]),
             new EditorButton(image),
             new EditorButton(horizontalRule),
+            new EditorButton(codeBlock),
             new EditorButton(details),
         ]),
 
index 7530382630bd309e744e50019f726250118a5322..5305ada823570c5cc3098095c94aa35b1c656677 100644 (file)
@@ -244,6 +244,13 @@ body.editor-is-fullscreen {
   cursor: row-resize;
 }
 
+.editor-code-block-wrap {
+  user-select: none;
+  > * {
+    pointer-events: none;
+  }
+}
+
 // Editor theme styles
 .editor-theme-bold {
   font-weight: bold;