1 import {EditorFormModal, EditorFormModalDefinition} from "./modals";
2 import {EditorContainerUiElement, EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
3 import {EditorDecorator, EditorDecoratorAdapter} from "./decorator";
4 import {$getSelection, COMMAND_PRIORITY_LOW, LexicalEditor, SELECTION_CHANGE_COMMAND} from "lexical";
5 import {DecoratorListener} from "lexical/LexicalEditor";
6 import type {NodeKey} from "lexical/LexicalNode";
7 import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
9 export class EditorUIManager {
11 protected modalDefinitionsByKey: Record<string, EditorFormModalDefinition> = {};
12 protected decoratorConstructorsByType: Record<string, typeof EditorDecorator> = {};
13 protected decoratorInstancesByNodeKey: Record<string, EditorDecorator> = {};
14 protected context: EditorUiContext|null = null;
15 protected toolbar: EditorContainerUiElement|null = null;
16 protected contextToolbarDefinitionsByKey: Record<string, EditorContextToolbarDefinition> = {};
17 protected activeContextToolbars: EditorContextToolbar[] = [];
19 setContext(context: EditorUiContext) {
20 this.context = context;
21 this.setupEditor(context.editor);
24 getContext(): EditorUiContext {
25 if (this.context === null) {
26 throw new Error(`Context attempted to be used without being set`);
32 triggerStateUpdateForElement(element: EditorUiElement) {
35 editor: this.getContext().editor
39 registerModal(key: string, modalDefinition: EditorFormModalDefinition) {
40 this.modalDefinitionsByKey[key] = modalDefinition;
43 createModal(key: string): EditorFormModal {
44 const modalDefinition = this.modalDefinitionsByKey[key];
45 if (!modalDefinition) {
46 throw new Error(`Attempted to show modal of key [${key}] but no modal registered for that key`);
49 const modal = new EditorFormModal(modalDefinition);
50 modal.setContext(this.getContext());
55 registerDecoratorType(type: string, decorator: typeof EditorDecorator) {
56 this.decoratorConstructorsByType[type] = decorator;
59 protected getDecorator(decoratorType: string, nodeKey: string): EditorDecorator {
60 if (this.decoratorInstancesByNodeKey[nodeKey]) {
61 return this.decoratorInstancesByNodeKey[nodeKey];
64 const decoratorClass = this.decoratorConstructorsByType[decoratorType];
65 if (!decoratorClass) {
66 throw new Error(`Attempted to use decorator of type [${decoratorType}] but not decorator registered for that type`);
70 const decorator = new decoratorClass(nodeKey);
71 this.decoratorInstancesByNodeKey[nodeKey] = decorator;
75 setToolbar(toolbar: EditorContainerUiElement) {
77 this.toolbar.getDOMElement().remove();
80 this.toolbar = toolbar;
81 toolbar.setContext(this.getContext());
82 this.getContext().containerDOM.prepend(toolbar.getDOMElement());
85 registerContextToolbar(key: string, definition: EditorContextToolbarDefinition) {
86 this.contextToolbarDefinitionsByKey[key] = definition;
89 protected triggerStateUpdate(update: EditorUiStateUpdate): void {
90 const context = this.getContext();
91 context.lastSelection = update.selection;
92 this.toolbar?.updateState(update);
93 this.updateContextToolbars(update);
94 for (const toolbar of this.activeContextToolbars) {
95 toolbar.updateState(update);
97 // console.log('selection update', update.selection);
100 triggerStateRefresh(): void {
101 this.triggerStateUpdate({
102 editor: this.getContext().editor,
103 selection: this.getContext().lastSelection,
107 protected updateContextToolbars(update: EditorUiStateUpdate): void {
108 for (const toolbar of this.activeContextToolbars) {
110 toolbar.getDOMElement().remove();
113 const node = (update.selection?.getNodes() || [])[0] || null;
118 const element = update.editor.getElementByKey(node.getKey());
123 const toolbarKeys = Object.keys(this.contextToolbarDefinitionsByKey);
124 const contentByTarget = new Map<HTMLElement, EditorUiElement[]>();
125 for (const key of toolbarKeys) {
126 const definition = this.contextToolbarDefinitionsByKey[key];
127 const matchingElem = ((element.closest(definition.selector)) || (element.querySelector(definition.selector))) as HTMLElement|null;
129 const targetEl = definition.displayTargetLocator ? definition.displayTargetLocator(matchingElem) : matchingElem;
130 if (!contentByTarget.has(targetEl)) {
131 contentByTarget.set(targetEl, [])
134 contentByTarget.get(targetEl).push(...definition.content);
138 for (const [target, contents] of contentByTarget) {
139 const toolbar = new EditorContextToolbar(contents);
140 toolbar.setContext(this.getContext());
141 this.activeContextToolbars.push(toolbar);
143 this.getContext().containerDOM.append(toolbar.getDOMElement());
144 toolbar.attachTo(target);
148 protected setupEditor(editor: LexicalEditor) {
149 // Update button states on editor selection change
150 editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
151 this.triggerStateUpdate({
153 selection: $getSelection(),
156 }, COMMAND_PRIORITY_LOW);
158 // Register our DOM decorate listener with the editor
159 const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => {
160 const keys = Object.keys(decorators);
161 for (const key of keys) {
162 const decoratedEl = editor.getElementByKey(key);
163 const adapter = decorators[key];
164 const decorator = this.getDecorator(adapter.type, key);
165 decorator.setNode(adapter.getNode());
166 const decoratorEl = decorator.render(this.getContext());
168 decoratedEl.append(decoratorEl);
172 editor.registerDecoratorListener(domDecorateListener);