From: Dan Brown Date: Tue, 24 Jun 2025 16:47:53 +0000 (+0100) Subject: Lexical: Started comment implementation X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/c606970e38625d1d41c0707c4aca37249fb55a1b?ds=sidebyside;hp=--cc Lexical: Started comment implementation Refactors some UI and toolbar code for better abstract use across editor versions. --- c606970e38625d1d41c0707c4aca37249fb55a1b diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 7ecf91d23..8e98780d5 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -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 = {}): 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 = {}): 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; diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index cdc451d08..fc413bb8f 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -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 = { + 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 diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 2d15b341b..c40206607 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -198,7 +198,7 @@ export class EditorUIManager { contentByTarget.set(targetEl, []) } // @ts-ignore - contentByTarget.get(targetEl).push(...definition.content); + contentByTarget.get(targetEl).push(...definition.content()); } } diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts index de2255444..323b17450 100644 --- a/resources/js/wysiwyg/ui/framework/toolbars.ts +++ b/resources/js/wysiwyg/ui/framework/toolbars.ts @@ -4,7 +4,7 @@ import {el} from "../../utils/dom"; export type EditorContextToolbarDefinition = { selector: string; - content: EditorUiElement[], + content: () => EditorUiElement[], displayTargetLocator?: (originalTarget: HTMLElement) => HTMLElement; }; diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index e7ec6adbc..c48386bb4 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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): 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): 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