]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/core.ts
Comments: Switched to lexical editor
[bookstack] / resources / js / wysiwyg / ui / framework / core.ts
1 import {BaseSelection, LexicalEditor} from "lexical";
2 import {EditorUIManager} from "./manager";
3
4 import {el} from "../../utils/dom";
5
6 export type EditorUiStateUpdate = {
7     editor: LexicalEditor;
8     selection: BaseSelection|null;
9 };
10
11 export type EditorUiContext = {
12     editor: LexicalEditor; // Lexical editor instance
13     editorDOM: HTMLElement; // DOM element the editor is bound to
14     containerDOM: HTMLElement; // DOM element which contains all editor elements
15     scrollDOM: HTMLElement; // DOM element which is the main content scroll container
16     translate: (text: string) => string; // Translate function
17     error: (text: string|Error) => void; // Error reporting function
18     manager: EditorUIManager; // UI Manager instance for this editor
19     options: Record<string, any>; // General user options which may be used by sub elements
20 };
21
22 export interface EditorUiBuilderDefinition {
23     build: () => EditorUiElement;
24 }
25
26 export function isUiBuilderDefinition(object: any): object is EditorUiBuilderDefinition {
27     return 'build' in object;
28 }
29
30 export abstract class EditorUiElement {
31     protected dom: HTMLElement|null = null;
32     private context: EditorUiContext|null = null;
33     private abortController: AbortController = new AbortController();
34
35     protected abstract buildDOM(): HTMLElement;
36
37     setContext(context: EditorUiContext): void {
38         this.context = context;
39     }
40
41     getContext(): EditorUiContext {
42         if (this.context === null) {
43             throw new Error('Attempted to use EditorUIContext before it has been set');
44         }
45
46         return this.context;
47     }
48
49     getDOMElement(): HTMLElement {
50         if (!this.dom) {
51             this.dom = this.buildDOM();
52         }
53
54         return this.dom;
55     }
56
57     rebuildDOM(): HTMLElement {
58         const newDOM = this.buildDOM();
59         this.dom?.replaceWith(newDOM);
60         this.dom = newDOM;
61         return this.dom;
62     }
63
64     trans(text: string) {
65         return this.getContext().translate(text);
66     }
67
68     updateState(state: EditorUiStateUpdate): void {
69         return;
70     }
71
72     emitEvent(name: string, data: object = {}): void {
73         if (this.dom) {
74             this.dom.dispatchEvent(new CustomEvent('editor::' + name, {detail: data, bubbles: true}));
75         }
76     }
77
78     onEvent(name: string, callback: (data: object) => any, listenTarget: HTMLElement|null = null): void {
79         const target = listenTarget || this.dom;
80         if (target) {
81             target.addEventListener('editor::' + name, ((event: CustomEvent) => {
82                 callback(event.detail);
83             }) as EventListener, { signal: this.abortController.signal });
84         }
85     }
86
87     teardown(): void {
88         if (this.dom && this.dom.isConnected) {
89             this.dom.remove();
90         }
91         this.abortController.abort('teardown');
92     }
93 }
94
95 export class EditorContainerUiElement extends EditorUiElement {
96     protected children : EditorUiElement[] = [];
97
98     constructor(children: EditorUiElement[]) {
99         super();
100         this.children.push(...children);
101     }
102
103     protected buildDOM(): HTMLElement {
104         return el('div', {}, this.getChildren().map(child => child.getDOMElement()));
105     }
106
107     getChildren(): EditorUiElement[] {
108         return this.children;
109     }
110
111     protected addChildren(...children: EditorUiElement[]): void {
112         this.children.push(...children);
113     }
114
115     protected removeChildren(...children: EditorUiElement[]): void {
116         for (const child of children) {
117             this.removeChild(child);
118         }
119     }
120
121     protected removeChild(child: EditorUiElement) {
122         const index = this.children.indexOf(child);
123         if (index !== -1) {
124             this.children.splice(index, 1);
125         }
126     }
127
128     updateState(state: EditorUiStateUpdate): void {
129         for (const child of this.children) {
130             child.updateState(state);
131         }
132     }
133
134     setContext(context: EditorUiContext) {
135         super.setContext(context);
136         for (const child of this.getChildren()) {
137             child.setContext(context);
138         }
139     }
140
141     teardown() {
142         for (const child of this.children) {
143             child.teardown();
144         }
145         super.teardown();
146     }
147 }
148
149 export class EditorSimpleClassContainer extends EditorContainerUiElement {
150     protected className;
151
152     constructor(className: string, children: EditorUiElement[]) {
153         super(children);
154         this.className = className;
155     }
156
157     protected buildDOM(): HTMLElement {
158         return el('div', {
159             class: this.className,
160         }, this.getChildren().map(child => child.getDOMElement()));
161     }
162 }
163