From 63f4b424534bcddbc89c0df6690ee277ebe236ef Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 19 Jul 2024 18:12:51 +0100 Subject: [PATCH] Lexical: Added toolbar scroll/resize handling 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 | 2 +- resources/js/wysiwyg/todo.md | 4 +- resources/js/wysiwyg/ui/framework/core.ts | 1 + resources/js/wysiwyg/ui/framework/manager.ts | 23 +++++++++--- resources/js/wysiwyg/ui/framework/toolbars.ts | 37 +++++++++++++++++-- resources/js/wysiwyg/ui/index.ts | 10 +++-- resources/sass/_editor.scss | 4 ++ 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 0aa04dfd9..5f131df57 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -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); diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index e0b58eef6..9950254df 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -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 diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index 22a821a89..c8f390c48 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index cfa94e8ae..29d959910 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts index c9db0d6bd..d7c481934 100644 --- a/resources/js/wysiwyg/ui/framework/toolbars.ts +++ b/resources/js/wysiwyg/ui/framework/toolbars.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 31407497f..f728ae48f 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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): EditorUiContext { +export function buildEditorUI(container: HTMLElement, element: HTMLElement, scrollContainer: HTMLElement, editor: LexicalEditor, options: Record): 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', diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index b577d1850..17e4af97b 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -161,6 +161,10 @@ body.editor-is-fullscreen { margin-left: -4px; top: -5px; } + &.is-above:before { + top: calc(100% - 5px); + transform: rotate(225deg); + } } // Modals