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 {
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;
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) {
--- /dev/null
+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
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";
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
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";
}
})
]);
+}
+
+export function getImageToolbarContent(): EditorUiElement[] {
+ return [new EditorButton(image)];
}
\ No newline at end of file