]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/manager.ts
Lexical: Reorganised some logic into manager
[bookstack] / resources / js / wysiwyg / ui / framework / manager.ts
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
8
9 export class EditorUIManager {
10
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
17     setContext(context: EditorUiContext) {
18         this.context = context;
19         this.setupEditor(context.editor);
20     }
21
22     getContext(): EditorUiContext {
23         if (this.context === null) {
24             throw new Error(`Context attempted to be used without being set`);
25         }
26
27         return this.context;
28     }
29
30     triggerStateUpdateForElement(element: EditorUiElement) {
31         element.updateState({
32             selection: null,
33             editor: this.getContext().editor
34         });
35     }
36
37     registerModal(key: string, modalDefinition: EditorFormModalDefinition) {
38         this.modalDefinitionsByKey[key] = modalDefinition;
39     }
40
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`);
45         }
46
47         const modal = new EditorFormModal(modalDefinition);
48         modal.setContext(this.getContext());
49
50         return modal;
51     }
52
53     registerDecoratorType(type: string, decorator: typeof EditorDecorator) {
54         this.decoratorConstructorsByType[type] = decorator;
55     }
56
57     protected getDecorator(decoratorType: string, nodeKey: string): EditorDecorator {
58         if (this.decoratorInstancesByNodeKey[nodeKey]) {
59             return this.decoratorInstancesByNodeKey[nodeKey];
60         }
61
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`);
65         }
66
67         // @ts-ignore
68         const decorator = new decoratorClass(nodeKey);
69         this.decoratorInstancesByNodeKey[nodeKey] = decorator;
70         return decorator;
71     }
72
73     setToolbar(toolbar: EditorContainerUiElement) {
74         if (this.toolbar) {
75             this.toolbar.getDOMElement().remove();
76         }
77
78         this.toolbar = toolbar;
79         toolbar.setContext(this.getContext());
80         this.getContext().editorDOM.before(toolbar.getDOMElement());
81     }
82
83     protected triggerStateUpdate(state: EditorUiStateUpdate): void {
84         const context = this.getContext();
85         context.lastSelection = state.selection;
86         this.toolbar?.updateState(state);
87     }
88
89     protected setupEditor(editor: LexicalEditor) {
90         // Update button states on editor selection change
91         editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
92             this.triggerStateUpdate({
93                 editor: editor,
94                 selection: $getSelection(),
95             });
96             return false;
97         }, COMMAND_PRIORITY_LOW);
98
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());
108                 if (decoratedEl) {
109                     decoratedEl.append(decoratorEl);
110                 }
111             }
112         }
113         editor.registerDecoratorListener(domDecorateListener);
114     }
115 }