]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Finished up core drawing insert/editing
authorDan Brown <redacted>
Fri, 19 Jul 2024 11:09:41 +0000 (12:09 +0100)
committerDan Brown <redacted>
Fri, 19 Jul 2024 11:09:41 +0000 (12:09 +0100)
Added new options that sits on the context, for things needed but not
for the core editor, which are defined out of the editor (drawio URL,
error message text, pageId etc...)

13 files changed:
resources/icons/editor/diagram.svg [new file with mode: 0644]
resources/js/components/wysiwyg-editor.js
resources/js/services/drawio.ts
resources/js/wysiwyg-tinymce/plugin-drawio.js
resources/js/wysiwyg/index.ts
resources/js/wysiwyg/nodes/diagram.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/decorators/diagram.ts
resources/js/wysiwyg/ui/defaults/button-definitions.ts
resources/js/wysiwyg/ui/framework/core.ts
resources/js/wysiwyg/ui/index.ts
resources/js/wysiwyg/ui/toolbars.ts
resources/sass/_editor.scss

diff --git a/resources/icons/editor/diagram.svg b/resources/icons/editor/diagram.svg
new file mode 100644 (file)
index 0000000..6ac78f5
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-60q-63 0-106.5-43.5T330-210q0-52 31-91.5t79-53.5v-85H200v-160H100v-280h280v280H280v80h400v-85q-48-14-79-53.5T570-750q0-63 43.5-106.5T720-900q63 0 106.5 43.5T870-750q0 52-31 91.5T760-605v165H520v85q48 14 79 53.5t31 91.5q0 63-43.5 106.5T480-60Zm240-620q29 0 49.5-20.5T790-750q0-29-20.5-49.5T720-820q-29 0-49.5 20.5T650-750q0 29 20.5 49.5T720-680Zm-540 0h120v-120H180v120Zm300 540q29 0 49.5-20.5T550-210q0-29-20.5-49.5T480-280q-29 0-49.5 20.5T410-210q0 29 20.5 49.5T480-140ZM240-740Zm480-10ZM480-210Z"/></svg>
\ No newline at end of file
index deb37186471440e00a73498c68ece6e0267d4b23..ebc142e2abc414184f58fff607eb0554d176bf10 100644 (file)
@@ -12,7 +12,14 @@ export class WysiwygEditor extends Component {
 
         window.importVersioned('wysiwyg').then(wysiwyg => {
             const editorContent = this.input.value;
-            this.editor = wysiwyg.createPageEditorInstance(this.editContainer, editorContent);
+            this.editor = wysiwyg.createPageEditorInstance(this.editContainer, editorContent, {
+                drawioUrl: this.getDrawIoUrl(),
+                pageId: Number(this.$opts.pageId),
+                translations: {
+                    imageUploadErrorText: this.$opts.imageUploadErrorText,
+                    serverUploadLimitText: this.$opts.serverUploadLimitText,
+                },
+            });
         });
 
         let handlingFormSubmit = false;
@@ -35,7 +42,6 @@ export class WysiwygEditor extends Component {
     }
 
     getDrawIoUrl() {
-        // TODO
         const drawioUrlElem = document.querySelector('[drawio-url]');
         if (drawioUrlElem) {
             return drawioUrlElem.getAttribute('drawio-url');
index c0a6b5044bc6256639abd691d5df52d617b1a1b3..4d7d88f1fdd26d4e84a6fc9f449d4c6a36528289 100644 (file)
@@ -127,13 +127,13 @@ export async function show(drawioUrl: string, onInitCallback: () => Promise<stri
     lastApprovedOrigin = (new URL(drawioUrl)).origin;
 }
 
-export async function upload(imageData: string, pageUploadedToId: string): Promise<{}|string> {
+export async function upload(imageData: string, pageUploadedToId: string): Promise<{id: number, url: string}> {
     const data = {
         image: imageData,
         uploaded_to: pageUploadedToId,
     };
     const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);
-    return resp.data;
+    return resp.data as {id: number, url: string};
 }
 
 export function close() {
index 3b343a9586b1db0640d722c5cfbef373bff332bc..342cac0af74c4df3677482e5efc8ba4a92e60f90 100644 (file)
@@ -1,4 +1,4 @@
-import * as DrawIO from '../services/drawio';
+import * as DrawIO from '../services/drawio.ts';
 import {wait} from '../services/util';
 
 let pageEditor = null;
index 8cbaccd7979761ef027faf9d5a03d3df960fc3d6..0aa04dfd9075ebbe8cefbeb2e0395b155835c9b4 100644 (file)
@@ -9,7 +9,7 @@ import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
 import {el} from "./helpers";
 import {EditorUiContext} from "./ui/framework/core";
 
-export function createPageEditorInstance(container: HTMLElement, htmlContent: string): SimpleWysiwygEditorInterface {
+export function createPageEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
     const config: CreateEditorArgs = {
         namespace: 'BookStackPageEditor',
         nodes: getNodesForPageEditor(),
@@ -60,7 +60,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st
         }
     });
 
-    const context: EditorUiContext = buildEditorUI(container, editArea, editor);
+    const context: EditorUiContext = buildEditorUI(container, editArea, editor, options);
     registerCommonNodeMutationListeners(context);
 
     return new SimpleWysiwygEditorInterface(editor);
index 15726813c9b0f3b7bb1e451c635ddefdd8a46997..1aff06400374ec45375d9da55371a8bc1cdc9194 100644 (file)
@@ -10,6 +10,9 @@ import {
 import type {EditorConfig} from "lexical/LexicalEditor";
 import {el} from "../helpers";
 import {EditorDecoratorAdapter} from "../ui/framework/decorator";
+import * as DrawIO from '../../services/drawio';
+import {EditorUiContext} from "../ui/framework/core";
+import {HttpError} from "../../services/http";
 
 export type SerializedDiagramNode = Spread<{
     id: string;
@@ -42,10 +45,10 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
         self.__drawingId = drawingId;
     }
 
-    getDrawingIdAndUrl(): {id: string, url: string} {
+    getDrawingIdAndUrl(): { id: string, url: string } {
         const self = this.getLatest();
         return {
-            id: self.__drawingUrl,
+            id: self.__drawingId,
             url: self.__drawingUrl,
         };
     }
@@ -103,16 +106,16 @@ export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
         return false;
     }
 
-    static importDOM(): DOMConversionMap|null {
+    static importDOM(): DOMConversionMap | null {
         return {
-            div(node: HTMLElement): DOMConversion|null {
+            div(node: HTMLElement): DOMConversion | null {
 
                 if (!node.hasAttribute('drawio-diagram')) {
                     return null;
                 }
 
                 return {
-                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+                    conversion: (element: HTMLElement): DOMConversionOutput | null => {
 
                         const img = element.querySelector('img');
                         const drawingUrl = img?.getAttribute('src') || '';
@@ -153,6 +156,64 @@ export function $isDiagramNode(node: LexicalNode | null | undefined) {
     return node instanceof DiagramNode;
 }
 
-export function $openDrawingEditorForNode(editor: LexicalEditor, node: DiagramNode): void {
-    // Todo
+
+function handleUploadError(error: HttpError, context: EditorUiContext): void {
+    if (error.status === 413) {
+        window.$events.emit('error', context.options.translations.serverUploadLimitText || '');
+    } else {
+        window.$events.emit('error', context.options.translations.imageUploadErrorText || '');
+    }
+    console.error(error);
+}
+
+async function loadDiagramIdFromNode(editor: LexicalEditor, node: DiagramNode): Promise<string> {
+    const drawingId = await new Promise<string>((res, rej) => {
+        editor.getEditorState().read(() => {
+            const {id: drawingId} = node.getDrawingIdAndUrl();
+            res(drawingId);
+        });
+    });
+
+    return drawingId || '';
+}
+
+async function updateDrawingNodeFromData(context: EditorUiContext, node: DiagramNode, pngData: string, isNew: boolean): Promise<void> {
+    DrawIO.close();
+
+    if (isNew) {
+        const loadingImage: string = window.baseUrl('/loading.gif');
+        context.editor.update(() => {
+            node.setDrawingIdAndUrl('', loadingImage);
+        });
+    }
+
+    try {
+        const img = await DrawIO.upload(pngData, context.options.pageId);
+        context.editor.update(() => {
+            node.setDrawingIdAndUrl(String(img.id), img.url);
+        });
+    } catch (err) {
+        if (err instanceof HttpError) {
+            handleUploadError(err, context);
+        }
+
+        if (isNew) {
+            context.editor.update(() => {
+                node.remove();
+            });
+        }
+
+        throw new Error(`Failed to save image with error: ${err}`);
+    }
+}
+
+export function $openDrawingEditorForNode(context: EditorUiContext, node: DiagramNode): void {
+    let isNew = false;
+    DrawIO.show(context.options.drawioUrl, async () => {
+        const drawingId = await loadDiagramIdFromNode(context.editor, node);
+        isNew = !drawingId;
+        return isNew ? '' : DrawIO.load(drawingId);
+    }, async (pngData: string) => {
+        return updateDrawingNodeFromData(context, node, pngData, isNew);
+    });
 }
\ No newline at end of file
index 61b592ca023e3c38b429a60d6ddd188fa5077e73..e0b58eef6a46438380055576174a94eda1610d10 100644 (file)
@@ -2,9 +2,6 @@
 
 ## In progress
 
-- Add Type: Drawings
-  - Continue converting drawio to typescript
-  - Next step to convert http service to ts.
 
 ## Main Todo
 
index 9c48f8c24727d37228ecba8ee970d281d847cc18..0f1263f38f408a223df3a41e99a1cc2f9cf13788 100644 (file)
@@ -1,7 +1,6 @@
 import {EditorDecorator} from "../framework/decorator";
 import {EditorUiContext} from "../framework/core";
 import {$selectionContainsNode, $selectSingleNode} from "../../helpers";
-import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
 import {BaseSelection} from "lexical";
 import {$openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
 
@@ -11,6 +10,7 @@ export class DiagramDecorator extends EditorDecorator {
 
     setup(context: EditorUiContext, element: HTMLElement) {
         const diagramNode = this.getNode();
+        element.classList.add('editor-diagram');
         element.addEventListener('click', event => {
             context.editor.update(() => {
                 $selectSingleNode(this.getNode());
@@ -19,7 +19,7 @@ export class DiagramDecorator extends EditorDecorator {
 
         element.addEventListener('dblclick', event => {
             context.editor.getEditorState().read(() => {
-                $openDrawingEditorForNode(context.editor, (this.getNode() as DiagramNode));
+                $openDrawingEditorForNode(context, (this.getNode() as DiagramNode));
             });
         });
 
index bf725f8c8c86f58afb8d482d75f5ae43584306a6..5316dacf7d34415b59635533c5f9591bf0d2cd87 100644 (file)
@@ -67,12 +67,14 @@ import tableIcon from "@icons/editor/table.svg";
 import imageIcon from "@icons/editor/image.svg";
 import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg";
 import codeBlockIcon from "@icons/editor/code-block.svg";
+import diagramIcon from "@icons/editor/diagram.svg";
 import detailsIcon from "@icons/editor/details.svg";
 import sourceIcon from "@icons/editor/source-view.svg";
 import fullscreenIcon from "@icons/editor/fullscreen.svg";
 import editIcon from "@icons/edit.svg";
 import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
 import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
+import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram";
 
 export const undo: EditorButtonDefinition = {
     label: 'Undo',
@@ -445,6 +447,31 @@ export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock
     icon: editIcon,
 });
 
+export const diagram: EditorButtonDefinition = {
+    label: 'Insert/edit drawing',
+    icon: diagramIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const selection = $getSelection();
+            const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null);
+            if (diagramNode === null) {
+                context.editor.update(() => {
+                    const diagram = $createDiagramNode();
+                    $insertNewBlockNodeAtSelection(diagram, true);
+                    $openDrawingEditorForNode(context, diagram);
+                    diagram.selectStart();
+                });
+            } else {
+                $openDrawingEditorForNode(context, diagramNode);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return $selectionContainsNodeType(selection, $isDiagramNode);
+    }
+};
+
+
 export const details: EditorButtonDefinition = {
     label: 'Insert collapsible block',
     icon: detailsIcon,
index 465765caa3181b4501d53d068c55e1b90809bc27..22a821a89b267d8c13b6729b4e2dba2f7c5e5f9b 100644 (file)
@@ -3,17 +3,18 @@ import {EditorUIManager} from "./manager";
 import {el} from "../../helpers";
 
 export type EditorUiStateUpdate = {
-    editor: LexicalEditor,
-    selection: BaseSelection|null,
+    editor: LexicalEditor;
+    selection: BaseSelection|null;
 };
 
 export type EditorUiContext = {
-    editor: LexicalEditor,
-    editorDOM: HTMLElement,
-    containerDOM: HTMLElement,
-    translate: (text: string) => string,
-    manager: EditorUIManager,
-    lastSelection: BaseSelection|null,
+    editor: LexicalEditor; // Lexical editor instance
+    editorDOM: HTMLElement; // DOM element the editor is bound to
+    containerDOM: HTMLElement; // DOM element which contains all editor elements
+    translate: (text: string) => string; // Translate function
+    manager: EditorUIManager; // UI Manager instance for this editor
+    lastSelection: BaseSelection|null; // The last tracked selection made by the user
+    options: Record<string, any>; // General user options which may be used by sub elements
 };
 
 export abstract class EditorUiElement {
index 748370959620b3bbdb9b20dc145be9ea0514ac14..31407497f767ae0b76ed60de1921246a95289568 100644 (file)
@@ -12,7 +12,7 @@ import {EditorUiContext} from "./framework/core";
 import {CodeBlockDecorator} from "./decorators/code-block";
 import {DiagramDecorator} from "./decorators/diagram";
 
-export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor): EditorUiContext {
+export function buildEditorUI(container: HTMLElement, element: HTMLElement, editor: LexicalEditor, options: Record<string, any>): EditorUiContext {
     const manager = new EditorUIManager();
     const context: EditorUiContext = {
         editor,
@@ -21,6 +21,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
         manager,
         translate: (text: string): string => text,
         lastSelection: null,
+        options,
     };
     manager.setContext(context);
 
@@ -43,7 +44,7 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, edit
 
     // Register context toolbars
     manager.registerContextToolbar('image', {
-        selector: 'img',
+        selector: 'img:not([drawio-diagram] img)',
         content: getImageToolbarContent(),
         displayTargetLocator(originalTarget: HTMLElement) {
             return originalTarget.closest('a') || originalTarget;
index 9145b8761931439d4cf80724bf53791ef3012327..f5eae6b212d9658c2f69092802e6300c24e15f49 100644 (file)
@@ -4,7 +4,7 @@ import {
     alignLeft,
     alignRight,
     blockquote, bold, bulletList, clearFormating, code, codeBlock,
-    dangerCallout, details, editCodeBlock, fullscreen,
+    dangerCallout, details, diagram, editCodeBlock, fullscreen,
     h2, h3, h4, h5, highlightColor, horizontalRule, image,
     infoCallout, italic, link, numberList, paragraph,
     redo, source, strikethrough, subscript,
@@ -89,6 +89,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
             new EditorButton(image),
             new EditorButton(horizontalRule),
             new EditorButton(codeBlock),
+            new EditorButton(diagram),
             new EditorButton(details),
         ]),
 
index 99045dd5ab1bdc24c96145116cad3693914f77bc..b577d185027935661d1146947a0c861a44595729 100644 (file)
@@ -316,6 +316,9 @@ body.editor-is-fullscreen {
     border: 1px dashed var(--editor-color-primary);
   }
 }
+.editor-diagram.selected {
+  outline: 2px dashed var(--editor-color-primary);
+}
 
 // Editor form elements
 .editor-form-field-wrapper {