]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added base context toolbar logic
authorDan Brown <redacted>
Sun, 30 Jun 2024 11:13:13 +0000 (12:13 +0100)
committerDan Brown <redacted>
Sun, 30 Jun 2024 11:13:13 +0000 (12:13 +0100)
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/framework/toolbars.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/index.ts
resources/js/wysiwyg/ui/toolbars.ts

index 2ef117b8836d3ad9e699de2645850b3413053fd1..685031ff871770333e1b6089e06f34afb5217ca8 100644 (file)
@@ -4,7 +4,7 @@ import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
 import {$getSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND} from "lexical";
 import {DecoratorListener} from "lexical/LexicalEditor";
 import type {NodeKey} from "lexical/LexicalNode";
-
+import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
 
 export class EditorUIManager {
 
@@ -13,6 +13,8 @@ export class EditorUIManager {
     protected decoratorInstancesByNodeKey: Record<string, EditorDecorator> = {};
     protected context: EditorUiContext|null = null;
     protected toolbar: EditorContainerUiElement|null = null;
+    protected contextToolbarDefinitionsByKey: Record<string, EditorContextToolbarDefinition> = {};
+    protected activeContextToolbars: EditorContextToolbar[] = [];
 
     setContext(context: EditorUiContext) {
         this.context = context;
@@ -80,10 +82,59 @@ export class EditorUIManager {
         this.getContext().editorDOM.before(toolbar.getDOMElement());
     }
 
-    protected triggerStateUpdate(state: EditorUiStateUpdate): void {
+    registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) {
+        this.contextToolbarDefinitionsByKey[key] = definition;
+    }
+
+    protected triggerStateUpdate(update: EditorUiStateUpdate): void {
         const context = this.getContext();
-        context.lastSelection = state.selection;
-        this.toolbar?.updateState(state);
+        context.lastSelection = update.selection;
+        this.toolbar?.updateState(update);
+        this.updateContextToolbars(update);
+        for (const toolbar of this.activeContextToolbars) {
+            toolbar.updateState(update);
+        }
+    }
+
+    protected updateContextToolbars(update: EditorUiStateUpdate): void {
+        for (const toolbar of this.activeContextToolbars) {
+            toolbar.empty();
+            toolbar.getDOMElement().remove();
+        }
+
+        const node = (update.selection?.getNodes() || [])[0] || null;
+        if (!node) {
+            return;
+        }
+
+        const element = update.editor.getElementByKey(node.getKey());
+        if (!element) {
+            return;
+        }
+
+        const toolbarKeys = Object.keys(this.contextToolbarDefinitionsByKey);
+        const contentByTarget = new Map<HTMLElement, EditorUiElement[]>();
+        for (const key of toolbarKeys) {
+            const definition = this.contextToolbarDefinitionsByKey[key];
+            const matchingElem = ((element.closest(definition.selector)) || (element.querySelector(definition.selector))) as HTMLElement|null;
+            if (matchingElem) {
+                const targetEl = definition.displayTargetLocator ? definition.displayTargetLocator(matchingElem) : matchingElem;
+                if (!contentByTarget.has(targetEl)) {
+                    contentByTarget.set(targetEl, [])
+                }
+                // @ts-ignore
+                contentByTarget.get(targetEl).push(...definition.content);
+            }
+        }
+
+        for (const [target, contents] of contentByTarget) {
+            const toolbar = new EditorContextToolbar(contents);
+            toolbar.setContext(this.getContext());
+            this.activeContextToolbars.push(toolbar);
+
+            this.getContext().editorDOM.after(toolbar.getDOMElement());
+            toolbar.attachTo(target);
+        }
     }
 
     protected setupEditor(editor: LexicalEditor) {
diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts
new file mode 100644 (file)
index 0000000..a844161
--- /dev/null
@@ -0,0 +1,36 @@
+import {EditorContainerUiElement, EditorUiElement} from "./core";
+import {el} from "../../helpers";
+
+export type EditorContextToolbarDefinition = {
+    selector: string;
+    content: EditorUiElement[],
+    displayTargetLocator?: (originalTarget: HTMLElement) => HTMLElement;
+};
+
+export class EditorContextToolbar extends EditorContainerUiElement {
+
+    protected buildDOM(): HTMLElement {
+        return el('div', {
+            class: 'editor-context-toolbar',
+        }, this.getChildren().map(child => child.getDOMElement()));
+    }
+
+    attachTo(target: HTMLElement) {
+        // Todo - attach to target position
+        console.log('attaching context toolbar to', target);
+    }
+
+    insert(children: EditorUiElement[]) {
+        this.addChildren(...children);
+        const dom = this.getDOMElement();
+        dom.append(...children.map(child => child.getDOMElement()));
+    }
+
+    empty() {
+        const children = this.getChildren();
+        for (const child of children) {
+            child.getDOMElement().remove();
+        }
+        this.removeChildren(...children);
+    }
+}
\ No newline at end of file
index c823f9f950077d47c5ba3b57514dbeb3ede0d9c3..9f05e211ed06949b2d993a0f1b565c4b8afe1075 100644 (file)
@@ -1,5 +1,5 @@
 import {LexicalEditor} from "lexical";
-import {getMainEditorFullToolbar} from "./toolbars";
+import {getImageToolbarContent, getMainEditorFullToolbar} from "./toolbars";
 import {EditorUIManager} from "./framework/manager";
 import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
 import {ImageDecorator} from "./decorators/image";
@@ -33,6 +33,15 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
         form: sourceFormDefinition,
     });
 
+    // Register context toolbars
+    manager.registerContextToolbar('image', {
+        selector: 'img',
+        content: getImageToolbarContent(),
+        displayTargetLocator(originalTarget: HTMLElement) {
+            return originalTarget.closest('a') || originalTarget;
+        }
+    });
+
     // Register image decorator listener
     manager.registerDecoratorType('image', ImageDecorator);
 }
\ No newline at end of file
index 02e46549e83d25a2d2801d480f190b5379bc350b..6e822e9c1396d04f28be689b0e4e591c56b1c1c6 100644 (file)
@@ -9,7 +9,7 @@ import {
     undo,
     warningCallout
 } from "./defaults/button-definitions";
-import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext} from "./framework/core";
+import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext, EditorUiElement} from "./framework/core";
 import {el} from "../helpers";
 import {EditorFormatMenu} from "./framework/blocks/format-menu";
 import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
@@ -87,4 +87,8 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
             }
         })
     ]);
+}
+
+export function getImageToolbarContent(): EditorUiElement[] {
+    return [new EditorButton(image)];
 }
\ No newline at end of file