]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started build of image node and decoration UI
authorDan Brown <redacted>
Mon, 3 Jun 2024 15:56:31 +0000 (16:56 +0100)
committerDan Brown <redacted>
Mon, 3 Jun 2024 15:56:31 +0000 (16:56 +0100)
resources/js/wysiwyg/nodes/image.ts [new file with mode: 0644]
resources/js/wysiwyg/nodes/index.ts
resources/js/wysiwyg/ui/framework/buttons.ts
resources/js/wysiwyg/ui/index.ts
resources/views/pages/parts/wysiwyg-editor.blade.php

diff --git a/resources/js/wysiwyg/nodes/image.ts b/resources/js/wysiwyg/nodes/image.ts
new file mode 100644 (file)
index 0000000..1e2cbd8
--- /dev/null
@@ -0,0 +1,174 @@
+import {
+    DecoratorNode,
+    DOMConversion,
+    DOMConversionMap,
+    DOMConversionOutput,
+    LexicalEditor, LexicalNode,
+    SerializedLexicalNode,
+    Spread
+} from "lexical";
+import type {EditorConfig} from "lexical/LexicalEditor";
+import {el} from "../helpers";
+
+export interface ImageNodeOptions {
+    alt?: string;
+    width?: number;
+    height?: number;
+}
+
+export type SerializedImageNode = Spread<{
+    src: string;
+    alt: string;
+    width: number;
+    height: number;
+}, SerializedLexicalNode>
+
+export class ImageNode extends DecoratorNode<HTMLElement> {
+    __src: string = '';
+    __alt: string = '';
+    __width: number = 0;
+    __height: number = 0;
+    // TODO - Alignment
+
+    static getType(): string {
+        return 'image';
+    }
+
+    static clone(node: ImageNode): ImageNode {
+        return new ImageNode(node.__src, {
+            alt: node.__alt,
+            width: node.__width,
+            height: node.__height,
+        });
+    }
+
+    constructor(src: string, options: ImageNodeOptions, key?: string) {
+        super(key);
+        this.__src = src;
+        if (options.alt) {
+            this.__alt = options.alt;
+        }
+        if (options.width) {
+            this.__width = options.width;
+        }
+        if (options.height) {
+            this.__height = options.height;
+        }
+    }
+
+    setAltText(altText: string): void {
+        const self = this.getWritable();
+        self.__alt = altText;
+    }
+
+    getAltText(): string {
+        const self = this.getLatest();
+        return self.__alt;
+    }
+
+    setHeight(height: number): void {
+        const self = this.getWritable();
+        self.__height = height;
+    }
+
+    getHeight(): number {
+        const self = this.getLatest();
+        return self.__height;
+    }
+
+    setWidth(width: number): void {
+        const self = this.getWritable();
+        self.__width = width;
+    }
+
+    getWidth(): number {
+        const self = this.getLatest();
+        return self.__width;
+    }
+
+    isInline(): boolean {
+        return true;
+    }
+
+    decorate(editor: LexicalEditor, config: EditorConfig): HTMLElement {
+        console.log('decorate!');
+        return el('div', {
+            class: 'editor-image-decorator',
+        }, ['decoration!!!']);
+    }
+
+    createDOM(_config: EditorConfig, _editor: LexicalEditor) {
+        const element = document.createElement('img');
+        element.setAttribute('src', this.__src);
+        element.textContent
+
+        if (this.__width) {
+            element.setAttribute('width', String(this.__width));
+        }
+        if (this.__height) {
+            element.setAttribute('height', String(this.__height));
+        }
+        if (this.__alt) {
+            element.setAttribute('alt', this.__alt);
+        }
+        return el('span', {class: 'editor-image-wrap'}, [
+            element,
+        ]);
+    }
+
+    updateDOM(prevNode: unknown, dom: HTMLElement) {
+        // Returning false tells Lexical that this node does not need its
+        // DOM element replacing with a new copy from createDOM.
+        return false;
+    }
+
+    static importDOM(): DOMConversionMap|null {
+        return {
+            img(node: HTMLElement): DOMConversion|null {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+
+                        const src = element.getAttribute('src') || '';
+                        const options: ImageNodeOptions = {
+                            alt: element.getAttribute('alt') || '',
+                            height: Number.parseInt(element.getAttribute('height') || '0'),
+                            width: Number.parseInt(element.getAttribute('width') || '0'),
+                        }
+
+                        return {
+                            node: new ImageNode(src, options),
+                        };
+                    },
+                    priority: 3,
+                };
+            },
+        };
+    }
+
+    exportJSON(): SerializedImageNode {
+        return {
+            type: 'image',
+            version: 1,
+            src: this.__src,
+            alt: this.__alt,
+            height: this.__height,
+            width: this.__width
+        };
+    }
+
+    static importJSON(serializedNode: SerializedImageNode): ImageNode {
+        return $createImageNode(serializedNode.src, {
+            alt: serializedNode.alt,
+            width: serializedNode.width,
+            height: serializedNode.height,
+        });
+    }
+}
+
+export function $createImageNode(src: string, options: ImageNodeOptions = {}): ImageNode {
+    return new ImageNode(src, options);
+}
+
+export function $isImageNode(node: LexicalNode | null | undefined) {
+    return node instanceof ImageNode;
+}
\ No newline at end of file
index 9f772df1e5c8232d3f614d7c4f67156024751b9d..1d492a87a25d2c0ddf218d7bd74441fae3b120ca 100644 (file)
@@ -3,6 +3,7 @@ import {CalloutNode} from './callout';
 import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
 import {CustomParagraphNode} from "./custom-paragraph";
 import {LinkNode} from "@lexical/link";
+import {ImageNode} from "./image";
 
 /**
  * Load the nodes for lexical.
@@ -12,6 +13,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         CalloutNode, // Todo - Create custom
         HeadingNode, // Todo - Create custom
         QuoteNode, // Todo - Create custom
+        ImageNode,
         CustomParagraphNode,
         {
             replace: ParagraphNode,
index 48046e9de9a953fcf435dead1db512b8075898ae..367a3933063523aa10af4bd7349ade84e18bf5e1 100644 (file)
@@ -67,7 +67,6 @@ export class FormatPreviewButton extends EditorButton {
         }, [this.getLabel()]);
 
         const stylesToApply = this.getStylesFromPreview();
-        console.log(stylesToApply);
         for (const style of Object.keys(stylesToApply)) {
             preview.style.setProperty(style, stylesToApply[style]);
         }
index 7e1f8d98173d10f68a773850bb01eb940cd9e000..9206f8b40568dfbcb9779671c8c7a349d7a62cd6 100644 (file)
@@ -7,6 +7,9 @@ import {
 import {getMainEditorFullToolbar} from "./toolbars";
 import {EditorUIManager} from "./framework/manager";
 import {link as linkFormDefinition} from "./defaults/form-definitions";
+import {DecoratorListener} from "lexical/LexicalEditor";
+import type {NodeKey} from "lexical/LexicalNode";
+import {el} from "../helpers";
 
 export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
     const manager = new EditorUIManager();
@@ -28,6 +31,20 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
         form: linkFormDefinition,
     });
 
+    // Register decorator listener
+    // Maybe move to manager?
+    const domDecorateListener: DecoratorListener<HTMLElement> = (decorator: Record<NodeKey, HTMLElement>) => {
+        const keys = Object.keys(decorator);
+        for (const key of keys) {
+            const decoratedEl = editor.getElementByKey(key);
+            const decoratorEl = decorator[key];
+            if (decoratedEl) {
+                decoratedEl.append(decoratorEl);
+            }
+        }
+    }
+    editor.registerDecoratorListener(domDecorateListener);
+
     // Update button states on editor selection change
     editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
         const selection = $getSelection();
index 940c005b5b737535353230e31f28cfa230221713..c0ceddc45d024c14bf5baa1867639d46e5169304 100644 (file)
@@ -9,13 +9,14 @@
     <div class="editor-container">
         <div refs="wysiwyg-editor@edit-area" contenteditable="true">
             <p id="Content!">Some <strong>content</strong> here</p>
+            <p>Content with image in, before text. <img src="https://bookstack.local/bookstack/uploads/images/gallery/2022-03/scaled-1680-/cats-image-2400x1000-2.jpg" width="200" alt="Sleepy meow"> After text.</p>
             <p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
             <h2>List below this h2 header</h2>
             <ul>
                 <li>Hello</li>
             </ul>
 
-            <p class="callout danger">
+            <p class="callout info">
                 Hello there, this is an info callout
             </p>
         </div>