]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added toolbar scroll/resize handling
authorDan Brown <redacted>
Fri, 19 Jul 2024 17:12:51 +0000 (18:12 +0100)
committerDan Brown <redacted>
Fri, 19 Jul 2024 17:12:51 +0000 (18:12 +0100)
Also added smarter above/below positioning to respond if toolbar would
be off the bottom of the editor, and added hide/show when they'd go
outside editor scroll bounds.

resources/js/wysiwyg/index.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/framework/core.ts
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/framework/toolbars.ts
resources/js/wysiwyg/ui/index.ts
resources/sass/_editor.scss

index 0aa04dfd9075ebbe8cefbeb2e0395b155835c9b4..5f131df5725562f1e7817423215551be66b2b7b9 100644 (file)
@@ -60,7 +60,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
         }
     });
 
-    const context: EditorUiContext = buildEditorUI(container, editArea, editor, options);
+    const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
     registerCommonNodeMutationListeners(context);
 
     return new SimpleWysiwygEditorInterface(editor);
index e0b58eef6a46438380055576174a94eda1610d10..9950254dfe679a246b0cbca1bb42b5cdc09fb263 100644 (file)
@@ -8,7 +8,6 @@
 - Alignments: Use existing classes for blocks
 - Alignments: Handle inline block content (image, video)
 - Add Type: Video/media/embed
-- Handle toolbars on scroll
 - Table features
 - Image paste upload
 - Keyboard shortcuts support
@@ -27,4 +26,5 @@
 ## Bugs
 
 - Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node.
-- Table resize bars often floating around in wrong place, and shows on hover or interrupts mouse actions.
\ No newline at end of file
+- Table resize bars often floating around in wrong place, and shows on hover or interrupts mouse actions.
+- Removing link around image via button deletes image, not just link 
\ No newline at end of file
index 22a821a89b267d8c13b6729b4e2dba2f7c5e5f9b..c8f390c4803c8fdcd01e67542af7e88dab127e33 100644 (file)
@@ -11,6 +11,7 @@ export type EditorUiContext = {
     editor: LexicalEditor; // Lexical editor instance
     editorDOM: HTMLElement; // DOM element the editor is bound to
     containerDOM: HTMLElement; // DOM element which contains all editor elements
+    scrollDOM: HTMLElement; // DOM element which is the main content scroll container
     translate: (text: string) => string; // Translate function
     manager: EditorUIManager; // UI Manager instance for this editor
     lastSelection: BaseSelection|null; // The last tracked selection made by the user
index cfa94e8aeecf03348225d967787f79a38445e670..29d959910757573d77484ef741c5a6b3281ef285 100644 (file)
@@ -21,6 +21,7 @@ export class EditorUIManager {
 
     setContext(context: EditorUiContext) {
         this.context = context;
+        this.setupEventListeners(context);
         this.setupEditor(context.editor);
     }
 
@@ -130,9 +131,10 @@ export class EditorUIManager {
     }
 
     protected updateContextToolbars(update: EditorUiStateUpdate): void {
-        for (const toolbar of this.activeContextToolbars) {
-            toolbar.empty();
-            toolbar.getDOMElement().remove();
+        for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {
+            const toolbar = this.activeContextToolbars[i];
+            toolbar.destroy();
+            this.activeContextToolbars.splice(i, 1);
         }
 
         const node = (update.selection?.getNodes() || [])[0] || null;
@@ -161,12 +163,12 @@ export class EditorUIManager {
         }
 
         for (const [target, contents] of contentByTarget) {
-            const toolbar = new EditorContextToolbar(contents);
+            const toolbar = new EditorContextToolbar(target, contents);
             toolbar.setContext(this.getContext());
             this.activeContextToolbars.push(toolbar);
 
             this.getContext().containerDOM.append(toolbar.getDOMElement());
-            toolbar.attachTo(target);
+            toolbar.updatePosition();
         }
     }
 
@@ -202,4 +204,15 @@ export class EditorUIManager {
         }
         editor.registerDecoratorListener(domDecorateListener);
     }
+
+    protected setupEventListeners(context: EditorUiContext) {
+        const updateToolbars = (event: Event) => {
+            for (const toolbar of this.activeContextToolbars) {
+                toolbar.updatePosition();
+            }
+        };
+
+        window.addEventListener('scroll', updateToolbars, {capture: true, passive: true});
+        window.addEventListener('resize', updateToolbars, {passive: true});
+    }
 }
\ No newline at end of file
index c9db0d6bd7f95040117a96e1611ae8f7432346b5..d7c48193479266ae51d3739b8bca005afb407d34 100644 (file)
@@ -9,20 +9,44 @@ export type EditorContextToolbarDefinition = {
 
 export class EditorContextToolbar extends EditorContainerUiElement {
 
+    protected target: HTMLElement;
+
+    constructor(target: HTMLElement, children: EditorUiElement[]) {
+        super(children);
+        this.target = target;
+    }
+
     protected buildDOM(): HTMLElement {
         return el('div', {
             class: 'editor-context-toolbar',
         }, this.getChildren().map(child => child.getDOMElement()));
     }
 
-    attachTo(target: HTMLElement) {
-        const targetBounds = target.getBoundingClientRect();
+    updatePosition() {
+        const editorBounds = this.getContext().scrollDOM.getBoundingClientRect();
+        const targetBounds = this.target.getBoundingClientRect();
         const dom = this.getDOMElement();
         const domBounds = dom.getBoundingClientRect();
 
+        const showing = targetBounds.bottom > editorBounds.top
+            && targetBounds.top < editorBounds.bottom;
+
+        dom.hidden = !showing;
+
+        if (!showing) {
+            return;
+        }
+
+        const showAbove: boolean = targetBounds.bottom + 6 + domBounds.height > editorBounds.bottom;
+        dom.classList.toggle('is-above', showAbove);
+
         const targetMid = targetBounds.left + (targetBounds.width / 2);
         const targetLeft = targetMid - (domBounds.width / 2);
-        dom.style.top = (targetBounds.bottom + 6) + 'px';
+        if (showAbove) {
+            dom.style.top = (targetBounds.top - 6 - domBounds.height) + 'px';
+        } else {
+            dom.style.top = (targetBounds.bottom + 6) + 'px';
+        }
         dom.style.left = targetLeft + 'px';
     }
 
@@ -32,11 +56,16 @@ export class EditorContextToolbar extends EditorContainerUiElement {
         dom.append(...children.map(child => child.getDOMElement()));
     }
 
-    empty() {
+    protected empty() {
         const children = this.getChildren();
         for (const child of children) {
             child.getDOMElement().remove();
         }
         this.removeChildren(...children);
     }
+
+    destroy() {
+        this.empty();
+        this.getDOMElement().remove();
+    }
 }
\ No newline at end of file
index 31407497f767ae0b76ed60de1921246a95289568..f728ae48fa52f348c6ee2aa91f41121140965aef 100644 (file)
@@ -12,12 +12,13 @@ import {EditorUiContext} from "./framework/core";
 import {CodeBlockDecorator} from "./decorators/code-block";
 import {DiagramDecorator} from "./decorators/diagram";
 
-export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
+export function buildEditorUI(container: HTMLElement, element: HTMLElement, scrollContainer: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
     const manager = new EditorUIManager();
     const context: EditorUiContext = {
         editor,
         containerDOM: container,
         editorDOM: element,
+        scrollDOM: scrollContainer,
         manager,
         translate: (text: string): string => text,
         lastSelection: null,
@@ -46,13 +47,14 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
     manager.registerContextToolbar('image', {
         selector: 'img:not([drawio-diagram] img)',
         content: getImageToolbarContent(),
-        displayTargetLocator(originalTarget: HTMLElement) {
-            return originalTarget.closest('a') || originalTarget;
-        }
     });
     manager.registerContextToolbar('link', {
         selector: 'a',
         content: getLinkToolbarContent(),
+        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
+            const image = originalTarget.querySelector('img');
+            return image || originalTarget;
+        }
     });
     manager.registerContextToolbar('code', {
         selector: '.editor-code-block-wrap',
index b577d185027935661d1146947a0c861a44595729..17e4af97bb0dcc64930bf6f4dd0d876ed2f89970 100644 (file)
@@ -161,6 +161,10 @@ body.editor-is-fullscreen {
     margin-left: -4px;
     top: -5px;
   }
+  &.is-above:before {
+    top: calc(100% - 5px);
+    transform: rotate(225deg);
+  }
 }
 
 // Modals