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";
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;
17 setContext(context: EditorUiContext) {
18 this.context = context;
19 this.setupEditor(context.editor);
22 getContext(): EditorUiContext {
23 if (this.context === null) {
24 throw new Error(`Context attempted to be used without being set`);
30 triggerStateUpdateForElement(element: EditorUiElement) {
33 editor: this.getContext().editor
37 registerModal(key: string, modalDefinition: EditorFormModalDefinition) {
38 this.modalDefinitionsByKey[key] = modalDefinition;
41 createModal(key: string): EditorFormModal {
42 const modalDefinition = this.modalDefinitionsByKey[key];
43 if (!modalDefinition) {
44 throw new Error(`Attempted to show modal of key [${key}] but no modal registered for that key`);
47 const modal = new EditorFormModal(modalDefinition);
48 modal.setContext(this.getContext());
53 registerDecoratorType(type: string, decorator: typeof EditorDecorator) {
54 this.decoratorConstructorsByType[type] = decorator;
57 protected getDecorator(decoratorType: string, nodeKey: string): EditorDecorator {
58 if (this.decoratorInstancesByNodeKey[nodeKey]) {
59 return this.decoratorInstancesByNodeKey[nodeKey];
62 const decoratorClass = this.decoratorConstructorsByType[decoratorType];
63 if (!decoratorClass) {
64 throw new Error(`Attempted to use decorator of type [${decoratorType}] but not decorator registered for that type`);
68 const decorator = new decoratorClass(nodeKey);
69 this.decoratorInstancesByNodeKey[nodeKey] = decorator;
73 setToolbar(toolbar: EditorContainerUiElement) {
75 this.toolbar.getDOMElement().remove();
78 this.toolbar = toolbar;
79 toolbar.setContext(this.getContext());
80 this.getContext().editorDOM.before(toolbar.getDOMElement());
83 protected triggerStateUpdate(state: EditorUiStateUpdate): void {
84 const context = this.getContext();
85 context.lastSelection = state.selection;
86 this.toolbar?.updateState(state);
89 protected setupEditor(editor: LexicalEditor) {
90 // Update button states on editor selection change
91 editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
92 this.triggerStateUpdate({
94 selection: $getSelection(),
97 }, COMMAND_PRIORITY_LOW);
99 // Register our DOM decorate listener with the editor
100 const domDecorateListener: DecoratorListener<EditorDecoratorAdapter> = (decorators: Record<NodeKey, EditorDecoratorAdapter>) => {
101 const keys = Object.keys(decorators);
102 for (const key of keys) {
103 const decoratedEl = editor.getElementByKey(key);
104 const adapter = decorators[key];
105 const decorator = this.getDecorator(adapter.type, key);
106 decorator.setNode(adapter.getNode());
107 const decoratorEl = decorator.render(this.getContext());
109 decoratedEl.append(decoratorEl);
113 editor.registerDecoratorListener(domDecorateListener);