]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started comment implementation
authorDan Brown <redacted>
Tue, 24 Jun 2025 16:47:53 +0000 (17:47 +0100)
committerDan Brown <redacted>
Tue, 24 Jun 2025 16:47:53 +0000 (17:47 +0100)
Refactors some UI and toolbar code for better abstract use across editor
versions.

resources/js/wysiwyg/index.ts
resources/js/wysiwyg/ui/defaults/toolbars.ts
resources/js/wysiwyg/ui/framework/manager.ts
resources/js/wysiwyg/ui/framework/toolbars.ts
resources/js/wysiwyg/ui/index.ts

index 7ecf91d230e4fa1671108306a611ee0f0e4f12c7..8e98780d58574c4ca98fc9fba2a904ddcd3e1e3d 100644 (file)
@@ -1,4 +1,4 @@
-import {$getSelection, createEditor, CreateEditorArgs, LexicalEditor} from 'lexical';
+import {createEditor, LexicalEditor} from 'lexical';
 import {createEmptyHistoryState, registerHistory} from '@lexical/history';
 import {registerRichText} from '@lexical/rich-text';
 import {mergeRegister} from '@lexical/utils';
@@ -11,65 +11,66 @@ import {listen as listenToCommonEvents} from "./services/common-events";
 import {registerDropPasteHandling} from "./services/drop-paste-handling";
 import {registerTaskListHandler} from "./ui/framework/helpers/task-list-handler";
 import {registerTableSelectionHandler} from "./ui/framework/helpers/table-selection-handler";
-import {el} from "./utils/dom";
 import {registerShortcuts} from "./services/shortcuts";
 import {registerNodeResizer} from "./ui/framework/helpers/node-resizer";
 import {registerKeyboardHandling} from "./services/keyboard-handling";
 import {registerAutoLinks} from "./services/auto-links";
+import {contextToolbars, getMainEditorFullToolbar} from "./ui/defaults/toolbars";
+import {modals} from "./ui/defaults/modals";
+import {CodeBlockDecorator} from "./ui/decorators/code-block";
+import {DiagramDecorator} from "./ui/decorators/diagram";
+
+const theme = {
+    text: {
+        bold: 'editor-theme-bold',
+        code: 'editor-theme-code',
+        italic: 'editor-theme-italic',
+        strikethrough: 'editor-theme-strikethrough',
+        subscript: 'editor-theme-subscript',
+        superscript: 'editor-theme-superscript',
+        underline: 'editor-theme-underline',
+        underlineStrikethrough: 'editor-theme-underline-strikethrough',
+    }
+};
 
 export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
-    const config: CreateEditorArgs = {
+    const editor = createEditor({
         namespace: 'BookStackPageEditor',
         nodes: getNodesForPageEditor(),
         onError: console.error,
-        theme: {
-            text: {
-                bold: 'editor-theme-bold',
-                code: 'editor-theme-code',
-                italic: 'editor-theme-italic',
-                strikethrough: 'editor-theme-strikethrough',
-                subscript: 'editor-theme-subscript',
-                superscript: 'editor-theme-superscript',
-                underline: 'editor-theme-underline',
-                underlineStrikethrough: 'editor-theme-underline-strikethrough',
-            }
-        }
-    };
-
-    const editArea = el('div', {
-        contenteditable: 'true',
-        class: 'editor-content-area page-content',
+        theme: theme,
     });
-    const editWrap = el('div', {
-        class: 'editor-content-wrap',
-    }, [editArea]);
-
-    container.append(editWrap);
-    container.classList.add('editor-container');
-    container.setAttribute('dir', options.textDirection);
-    if (options.darkMode) {
-        container.classList.add('editor-dark');
-    }
-
-    const editor = createEditor(config);
-    editor.setRootElement(editArea);
-    const context: EditorUiContext = buildEditorUI(container, editArea, editWrap, editor, options);
+    const context: EditorUiContext = buildEditorUI(container, editor, {
+        ...options,
+        editorClass: 'page-content',
+    });
+    editor.setRootElement(context.editorDOM);
 
     mergeRegister(
         registerRichText(editor),
         registerHistory(editor, createEmptyHistoryState(), 300),
         registerShortcuts(context),
         registerKeyboardHandling(context),
-        registerTableResizer(editor, editWrap),
+        registerTableResizer(editor, context.scrollDOM),
         registerTableSelectionHandler(editor),
-        registerTaskListHandler(editor, editArea),
+        registerTaskListHandler(editor, context.editorDOM),
         registerDropPasteHandling(context),
         registerNodeResizer(context),
         registerAutoLinks(editor),
     );
 
-    listenToCommonEvents(editor);
+    // Register toolbars, modals & decorators
+    context.manager.setToolbar(getMainEditorFullToolbar(context));
+    for (const key of Object.keys(contextToolbars)) {
+        context.manager.registerContextToolbar(key, contextToolbars[key]);
+    }
+    for (const key of Object.keys(modals)) {
+        context.manager.registerModal(key, modals[key]);
+    }
+    context.manager.registerDecoratorType('code', CodeBlockDecorator);
+    context.manager.registerDecoratorType('diagram', DiagramDecorator);
 
+    listenToCommonEvents(editor);
     setEditorContentFromHtml(editor, htmlContent);
 
     const debugView = document.getElementById('lexical-debug');
@@ -92,6 +93,33 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
     return new SimpleWysiwygEditorInterface(editor);
 }
 
+export function createCommentEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
+    const editor = createEditor({
+        namespace: 'BookStackCommentEditor',
+        nodes: getNodesForPageEditor(),
+        onError: console.error,
+        theme: theme,
+    });
+    const context: EditorUiContext = buildEditorUI(container, editor, options);
+    editor.setRootElement(context.editorDOM);
+
+    mergeRegister(
+        registerRichText(editor),
+        registerHistory(editor, createEmptyHistoryState(), 300),
+        registerShortcuts(context),
+        registerAutoLinks(editor),
+    );
+
+    // Register toolbars, modals & decorators
+    context.manager.setToolbar(getMainEditorFullToolbar(context)); // TODO - Create comment toolbar
+    context.manager.registerContextToolbar('link', contextToolbars.link);
+    context.manager.registerModal('link', modals.link);
+
+    setEditorContentFromHtml(editor, htmlContent);
+
+    return new SimpleWysiwygEditorInterface(editor);
+}
+
 export class SimpleWysiwygEditorInterface {
     protected editor: LexicalEditor;
 
index cdc451d088bc08c02a003eac320773b868996f6b..fc413bb8f80bed8221440dd4382208c0acb87626 100644 (file)
@@ -79,6 +79,7 @@ import {
 import {el} from "../../utils/dom";
 import {EditorButtonWithMenu} from "../framework/blocks/button-with-menu";
 import {EditorSeparator} from "../framework/blocks/separator";
+import {EditorContextToolbarDefinition} from "../framework/toolbars";
 
 export function getMainEditorFullToolbar(context: EditorUiContext): EditorContainerUiElement {
 
@@ -220,50 +221,64 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai
     ]);
 }
 
-export function getImageToolbarContent(): EditorUiElement[] {
-    return [new EditorButton(image)];
-}
-
-export function getMediaToolbarContent(): EditorUiElement[] {
-    return [new EditorButton(media)];
-}
-
-export function getLinkToolbarContent(): EditorUiElement[] {
-    return [
-        new EditorButton(link),
-        new EditorButton(unlink),
-    ];
-}
-
-export function getCodeToolbarContent(): EditorUiElement[] {
-    return [
-        new EditorButton(editCodeBlock),
-    ];
-}
-
-export function getTableToolbarContent(): EditorUiElement[] {
-    return [
-        new EditorOverflowContainer(2, [
-            new EditorButton(tableProperties),
-            new EditorButton(deleteTable),
-        ]),
-        new EditorOverflowContainer(3, [
-            new EditorButton(insertRowAbove),
-            new EditorButton(insertRowBelow),
-            new EditorButton(deleteRow),
-        ]),
-        new EditorOverflowContainer(3, [
-            new EditorButton(insertColumnBefore),
-            new EditorButton(insertColumnAfter),
-            new EditorButton(deleteColumn),
-        ]),
-    ];
-}
-
-export function getDetailsToolbarContent(): EditorUiElement[] {
-    return [
-        new EditorButton(detailsEditLabel),
-        new EditorButton(detailsToggle),
-        new EditorButton(detailsUnwrap),
-    ];
-}
\ No newline at end of file
+export const contextToolbars: Record<string, EditorContextToolbarDefinition> = {
+    image: {
+        selector: 'img:not([drawio-diagram] img)',
+        content: () => [new EditorButton(image)],
+    },
+    media: {
+        selector: '.editor-media-wrap',
+        content: () => [new EditorButton(media)],
+    },
+    link: {
+        selector: 'a',
+        content() {
+            return [
+                new EditorButton(link),
+                new EditorButton(unlink),
+            ]
+        },
+        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
+            const image = originalTarget.querySelector('img');
+            return image || originalTarget;
+        }
+    },
+    code: {
+        selector: '.editor-code-block-wrap',
+        content: () => [new EditorButton(editCodeBlock)],
+    },
+    table: {
+        selector: 'td,th',
+        content() {
+            return [
+                new EditorOverflowContainer(2, [
+                    new EditorButton(tableProperties),
+                    new EditorButton(deleteTable),
+                ]),
+                new EditorOverflowContainer(3, [
+                    new EditorButton(insertRowAbove),
+                    new EditorButton(insertRowBelow),
+                    new EditorButton(deleteRow),
+                ]),
+                new EditorOverflowContainer(3, [
+                    new EditorButton(insertColumnBefore),
+                    new EditorButton(insertColumnAfter),
+                    new EditorButton(deleteColumn),
+                ]),
+            ];
+        },
+        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
+            return originalTarget.closest('table') as HTMLTableElement;
+        }
+    },
+    details: {
+        selector: 'details',
+        content() {
+            return [
+                new EditorButton(detailsEditLabel),
+                new EditorButton(detailsToggle),
+                new EditorButton(detailsUnwrap),
+            ]
+        },
+    },
+};
\ No newline at end of file
index 2d15b341bdba6ae9a39e9236c77e5a6742854c4f..c40206607c62585da1140557a72104d388314752 100644 (file)
@@ -198,7 +198,7 @@ export class EditorUIManager {
                     contentByTarget.set(targetEl, [])
                 }
                 // @ts-ignore
-                contentByTarget.get(targetEl).push(...definition.content);
+                contentByTarget.get(targetEl).push(...definition.content());
             }
         }
 
index de2255444ebf6fcfc43fb7a9a225c3f36c208cc1..323b17450085ce4a3ddc30e13d32b3ee2311db01 100644 (file)
@@ -4,7 +4,7 @@ import {el} from "../../utils/dom";
 
 export type EditorContextToolbarDefinition = {
     selector: string;
-    content: EditorUiElement[],
+    content: () => EditorUiElement[],
     displayTargetLocator?: (originalTarget: HTMLElement) => HTMLElement;
 };
 
index e7ec6adbcd56b7eff8e6676b89220707ad329617..c48386bb4920e196eba48be07eca100bc713ae0f 100644 (file)
@@ -1,23 +1,30 @@
 import {LexicalEditor} from "lexical";
-import {
-    getCodeToolbarContent, getDetailsToolbarContent,
-    getImageToolbarContent,
-    getLinkToolbarContent,
-    getMainEditorFullToolbar, getMediaToolbarContent, getTableToolbarContent
-} from "./defaults/toolbars";
 import {EditorUIManager} from "./framework/manager";
 import {EditorUiContext} from "./framework/core";
-import {CodeBlockDecorator} from "./decorators/code-block";
-import {DiagramDecorator} from "./decorators/diagram";
-import {modals} from "./defaults/modals";
+import {el} from "../utils/dom";
+
+export function buildEditorUI(containerDOM: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
+    const editorDOM = el('div', {
+        contenteditable: 'true',
+        class: `editor-content-area ${options.editorClass || ''}`,
+    });
+    const scrollDOM = el('div', {
+        class: 'editor-content-wrap',
+    }, [editorDOM]);
+
+    containerDOM.append(scrollDOM);
+    containerDOM.classList.add('editor-container');
+    containerDOM.setAttribute('dir', options.textDirection);
+    if (options.darkMode) {
+        containerDOM.classList.add('editor-dark');
+    }
 
-export function buildEditorUI(container: HTMLElement, element: HTMLElement, scrollContainer: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
     const manager = new EditorUIManager();
     const context: EditorUiContext = {
         editor,
-        containerDOM: container,
-        editorDOM: element,
-        scrollDOM: scrollContainer,
+        containerDOM: containerDOM,
+        editorDOM: editorDOM,
+        scrollDOM: scrollDOM,
         manager,
         translate(text: string): string {
             const translations = options.translations;
@@ -31,50 +38,5 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
     };
     manager.setContext(context);
 
-    // Create primary toolbar
-    manager.setToolbar(getMainEditorFullToolbar(context));
-
-    // Register modals
-    for (const key of Object.keys(modals)) {
-        manager.registerModal(key, modals[key]);
-    }
-
-    // Register context toolbars
-    manager.registerContextToolbar('image', {
-        selector: 'img:not([drawio-diagram] img)',
-        content: getImageToolbarContent(),
-    });
-    manager.registerContextToolbar('media', {
-        selector: '.editor-media-wrap',
-        content: getMediaToolbarContent(),
-    });
-    manager.registerContextToolbar('link', {
-        selector: 'a',
-        content: getLinkToolbarContent(),
-        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
-            const image = originalTarget.querySelector('img');
-            return image || originalTarget;
-        }
-    });
-    manager.registerContextToolbar('code', {
-        selector: '.editor-code-block-wrap',
-        content: getCodeToolbarContent(),
-    });
-    manager.registerContextToolbar('table', {
-        selector: 'td,th',
-        content: getTableToolbarContent(),
-        displayTargetLocator(originalTarget: HTMLElement): HTMLElement {
-            return originalTarget.closest('table') as HTMLTableElement;
-        }
-    });
-    manager.registerContextToolbar('details', {
-        selector: 'details',
-        content: getDetailsToolbarContent(),
-    });
-
-    // Register image decorator listener
-    manager.registerDecoratorType('code', CodeBlockDecorator);
-    manager.registerDecoratorType('diagram', DiagramDecorator);
-
     return context;
 }
\ No newline at end of file