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.
This commit is contained in:
Dan Brown 2024-07-19 18:12:51 +01:00
parent c7c0df0964
commit 63f4b42453
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 65 additions and 16 deletions

View 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); registerCommonNodeMutationListeners(context);
return new SimpleWysiwygEditorInterface(editor); return new SimpleWysiwygEditorInterface(editor);

View File

@ -8,7 +8,6 @@
- Alignments: Use existing classes for blocks - Alignments: Use existing classes for blocks
- Alignments: Handle inline block content (image, video) - Alignments: Handle inline block content (image, video)
- Add Type: Video/media/embed - Add Type: Video/media/embed
- Handle toolbars on scroll
- Table features - Table features
- Image paste upload - Image paste upload
- Keyboard shortcuts support - Keyboard shortcuts support
@ -28,3 +27,4 @@
- Image resizing currently bugged, maybe change to ghost resizer in decorator instead of updating core node. - 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. - 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

View File

@ -11,6 +11,7 @@ export type EditorUiContext = {
editor: LexicalEditor; // Lexical editor instance editor: LexicalEditor; // Lexical editor instance
editorDOM: HTMLElement; // DOM element the editor is bound to editorDOM: HTMLElement; // DOM element the editor is bound to
containerDOM: HTMLElement; // DOM element which contains all editor elements 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 translate: (text: string) => string; // Translate function
manager: EditorUIManager; // UI Manager instance for this editor manager: EditorUIManager; // UI Manager instance for this editor
lastSelection: BaseSelection|null; // The last tracked selection made by the user lastSelection: BaseSelection|null; // The last tracked selection made by the user

View File

@ -21,6 +21,7 @@ export class EditorUIManager {
setContext(context: EditorUiContext) { setContext(context: EditorUiContext) {
this.context = context; this.context = context;
this.setupEventListeners(context);
this.setupEditor(context.editor); this.setupEditor(context.editor);
} }
@ -130,9 +131,10 @@ export class EditorUIManager {
} }
protected updateContextToolbars(update: EditorUiStateUpdate): void { protected updateContextToolbars(update: EditorUiStateUpdate): void {
for (const toolbar of this.activeContextToolbars) { for (let i = this.activeContextToolbars.length - 1; i >= 0; i--) {
toolbar.empty(); const toolbar = this.activeContextToolbars[i];
toolbar.getDOMElement().remove(); toolbar.destroy();
this.activeContextToolbars.splice(i, 1);
} }
const node = (update.selection?.getNodes() || [])[0] || null; const node = (update.selection?.getNodes() || [])[0] || null;
@ -161,12 +163,12 @@ export class EditorUIManager {
} }
for (const [target, contents] of contentByTarget) { for (const [target, contents] of contentByTarget) {
const toolbar = new EditorContextToolbar(contents); const toolbar = new EditorContextToolbar(target, contents);
toolbar.setContext(this.getContext()); toolbar.setContext(this.getContext());
this.activeContextToolbars.push(toolbar); this.activeContextToolbars.push(toolbar);
this.getContext().containerDOM.append(toolbar.getDOMElement()); this.getContext().containerDOM.append(toolbar.getDOMElement());
toolbar.attachTo(target); toolbar.updatePosition();
} }
} }
@ -202,4 +204,15 @@ export class EditorUIManager {
} }
editor.registerDecoratorListener(domDecorateListener); 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});
}
} }

View File

@ -9,20 +9,44 @@ export type EditorContextToolbarDefinition = {
export class EditorContextToolbar extends EditorContainerUiElement { export class EditorContextToolbar extends EditorContainerUiElement {
protected target: HTMLElement;
constructor(target: HTMLElement, children: EditorUiElement[]) {
super(children);
this.target = target;
}
protected buildDOM(): HTMLElement { protected buildDOM(): HTMLElement {
return el('div', { return el('div', {
class: 'editor-context-toolbar', class: 'editor-context-toolbar',
}, this.getChildren().map(child => child.getDOMElement())); }, this.getChildren().map(child => child.getDOMElement()));
} }
attachTo(target: HTMLElement) { updatePosition() {
const targetBounds = target.getBoundingClientRect(); const editorBounds = this.getContext().scrollDOM.getBoundingClientRect();
const targetBounds = this.target.getBoundingClientRect();
const dom = this.getDOMElement(); const dom = this.getDOMElement();
const domBounds = dom.getBoundingClientRect(); 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 targetMid = targetBounds.left + (targetBounds.width / 2);
const targetLeft = targetMid - (domBounds.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'; dom.style.left = targetLeft + 'px';
} }
@ -32,11 +56,16 @@ export class EditorContextToolbar extends EditorContainerUiElement {
dom.append(...children.map(child => child.getDOMElement())); dom.append(...children.map(child => child.getDOMElement()));
} }
empty() { protected empty() {
const children = this.getChildren(); const children = this.getChildren();
for (const child of children) { for (const child of children) {
child.getDOMElement().remove(); child.getDOMElement().remove();
} }
this.removeChildren(...children); this.removeChildren(...children);
} }
destroy() {
this.empty();
this.getDOMElement().remove();
}
} }

View File

@ -12,12 +12,13 @@ import {EditorUiContext} from "./framework/core";
import {CodeBlockDecorator} from "./decorators/code-block"; import {CodeBlockDecorator} from "./decorators/code-block";
import {DiagramDecorator} from "./decorators/diagram"; 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 manager = new EditorUIManager();
const context: EditorUiContext = { const context: EditorUiContext = {
editor, editor,
containerDOM: container, containerDOM: container,
editorDOM: element, editorDOM: element,
scrollDOM: scrollContainer,
manager, manager,
translate: (text: string): string => text, translate: (text: string): string => text,
lastSelection: null, lastSelection: null,
@ -46,13 +47,14 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
manager.registerContextToolbar('image', { manager.registerContextToolbar('image', {
selector: 'img:not([drawio-diagram] img)', selector: 'img:not([drawio-diagram] img)',
content: getImageToolbarContent(), content: getImageToolbarContent(),
displayTargetLocator(originalTarget: HTMLElement) {
return originalTarget.closest('a') || originalTarget;
}
}); });
manager.registerContextToolbar('link', { manager.registerContextToolbar('link', {
selector: 'a', selector: 'a',
content: getLinkToolbarContent(), content: getLinkToolbarContent(),
displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
const image = originalTarget.querySelector('img');
return image || originalTarget;
}
}); });
manager.registerContextToolbar('code', { manager.registerContextToolbar('code', {
selector: '.editor-code-block-wrap', selector: '.editor-code-block-wrap',

View File

@ -161,6 +161,10 @@ body.editor-is-fullscreen {
margin-left: -4px; margin-left: -4px;
top: -5px; top: -5px;
} }
&.is-above:before {
top: calc(100% - 5px);
transform: rotate(225deg);
}
} }
// Modals // Modals