]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/index.ts
Merge pull request #5731 from BookStackApp/lexical_jul25
[bookstack] / resources / js / wysiwyg / index.ts
1 import {createEditor, LexicalEditor} from 'lexical';
2 import {createEmptyHistoryState, registerHistory} from '@lexical/history';
3 import {registerRichText} from '@lexical/rich-text';
4 import {mergeRegister} from '@lexical/utils';
5 import {getNodesForBasicEditor, getNodesForPageEditor, registerCommonNodeMutationListeners} from './nodes';
6 import {buildEditorUI} from "./ui";
7 import {focusEditor, getEditorContentAsHtml, setEditorContentFromHtml} from "./utils/actions";
8 import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
9 import {EditorUiContext} from "./ui/framework/core";
10 import {listen as listenToCommonEvents} from "./services/common-events";
11 import {registerDropPasteHandling} from "./services/drop-paste-handling";
12 import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
13 import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
14 import {registerShortcuts} from "./services/shortcuts";
15 import {registerNodeResizer} from "./ui/framework/helpers/node-resizer";
16 import {registerKeyboardHandling} from "./services/keyboard-handling";
17 import {registerAutoLinks} from "./services/auto-links";
18 import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "./ui/defaults/toolbars";
19 import {modals} from "./ui/defaults/modals";
20 import {CodeBlockDecorator} from "./ui/decorators/code-block";
21 import {DiagramDecorator} from "./ui/decorators/diagram";
22 import {registerMouseHandling} from "./services/mouse-handling";
23
24 const theme = {
25     text: {
26         bold: 'editor-theme-bold',
27         code: 'editor-theme-code',
28         italic: 'editor-theme-italic',
29         strikethrough: 'editor-theme-strikethrough',
30         subscript: 'editor-theme-subscript',
31         superscript: 'editor-theme-superscript',
32         underline: 'editor-theme-underline',
33         underlineStrikethrough: 'editor-theme-underline-strikethrough',
34     }
35 };
36
37 export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
38     const editor = createEditor({
39         namespace: 'BookStackPageEditor',
40         nodes: getNodesForPageEditor(),
41         onError: console.error,
42         theme: theme,
43     });
44     const context: EditorUiContext = buildEditorUI(container, editor, {
45         ...options,
46         editorClass: 'page-content',
47     });
48     editor.setRootElement(context.editorDOM);
49
50     mergeRegister(
51         registerRichText(editor),
52         registerHistory(editor, createEmptyHistoryState(), 300),
53         registerShortcuts(context),
54         registerKeyboardHandling(context),
55         registerMouseHandling(context),
56         registerTableResizer(editor, context.scrollDOM),
57         registerTableSelectionHandler(editor),
58         registerTaskListHandler(editor, context.editorDOM),
59         registerDropPasteHandling(context),
60         registerNodeResizer(context),
61         registerAutoLinks(editor),
62     );
63
64     // Register toolbars, modals & decorators
65     context.manager.setToolbar(getMainEditorFullToolbar(context));
66     for (const key of Object.keys(contextToolbars)) {
67         context.manager.registerContextToolbar(key, contextToolbars[key]);
68     }
69     for (const key of Object.keys(modals)) {
70         context.manager.registerModal(key, modals[key]);
71     }
72     context.manager.registerDecoratorType('code', CodeBlockDecorator);
73     context.manager.registerDecoratorType('diagram', DiagramDecorator);
74
75     listenToCommonEvents(editor);
76     setEditorContentFromHtml(editor, htmlContent);
77
78     const debugView = document.getElementById('lexical-debug');
79     if (debugView) {
80         debugView.hidden = true;
81         editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => {
82             // Debug logic
83             // console.log('editorState', editorState.toJSON());
84             debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
85         });
86     }
87
88     // @ts-ignore
89     window.debugEditorState = () => {
90         return editor.getEditorState().toJSON();
91     };
92
93     registerCommonNodeMutationListeners(context);
94
95     return new SimpleWysiwygEditorInterface(context);
96 }
97
98 export function createBasicEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
99     const editor = createEditor({
100         namespace: 'BookStackBasicEditor',
101         nodes: getNodesForBasicEditor(),
102         onError: console.error,
103         theme: theme,
104     });
105     const context: EditorUiContext = buildEditorUI(container, editor, options);
106     editor.setRootElement(context.editorDOM);
107
108     const editorTeardown = mergeRegister(
109         registerRichText(editor),
110         registerHistory(editor, createEmptyHistoryState(), 300),
111         registerShortcuts(context),
112         registerAutoLinks(editor),
113     );
114
115     // Register toolbars, modals & decorators
116     context.manager.setToolbar(getBasicEditorToolbar(context));
117     context.manager.registerContextToolbar('link', contextToolbars.link);
118     context.manager.registerModal('link', modals.link);
119     context.manager.onTeardown(editorTeardown);
120
121     setEditorContentFromHtml(editor, htmlContent);
122
123     return new SimpleWysiwygEditorInterface(context);
124 }
125
126 export class SimpleWysiwygEditorInterface {
127     protected context: EditorUiContext;
128     protected onChangeListeners: (() => void)[] = [];
129     protected editorListenerTeardown: (() => void)|null = null;
130
131     constructor(context: EditorUiContext) {
132         this.context = context;
133     }
134
135     async getContentAsHtml(): Promise<string> {
136         return await getEditorContentAsHtml(this.context.editor);
137     }
138
139     onChange(listener: () => void) {
140         this.onChangeListeners.push(listener);
141         this.startListeningToChanges();
142     }
143
144     focus(): void {
145         focusEditor(this.context.editor);
146     }
147
148     remove() {
149         this.context.manager.teardown();
150         this.context.containerDOM.remove();
151         if (this.editorListenerTeardown) {
152             this.editorListenerTeardown();
153         }
154     }
155
156     protected startListeningToChanges(): void {
157         if (this.editorListenerTeardown) {
158             return;
159         }
160
161         this.editorListenerTeardown = this.context.editor.registerUpdateListener(() => {
162              for (const listener of this.onChangeListeners) {
163                  listener();
164              }
165         });
166     }
167 }