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:
parent
c7c0df0964
commit
63f4b42453
|
@ -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);
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue