From c7c0df096487a10f879d2a427373c5198bf2435c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 19 Jul 2024 12:09:41 +0100 Subject: [PATCH] Lexical: Finished up core drawing insert/editing 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...) --- resources/icons/editor/diagram.svg | 1 + resources/js/components/wysiwyg-editor.js | 10 ++- resources/js/services/drawio.ts | 4 +- resources/js/wysiwyg-tinymce/plugin-drawio.js | 2 +- resources/js/wysiwyg/index.ts | 4 +- resources/js/wysiwyg/nodes/diagram.ts | 75 +++++++++++++++++-- resources/js/wysiwyg/todo.md | 3 - resources/js/wysiwyg/ui/decorators/diagram.ts | 4 +- .../wysiwyg/ui/defaults/button-definitions.ts | 27 +++++++ resources/js/wysiwyg/ui/framework/core.ts | 17 +++-- resources/js/wysiwyg/ui/index.ts | 5 +- resources/js/wysiwyg/ui/toolbars.ts | 3 +- resources/sass/_editor.scss | 3 + 13 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 resources/icons/editor/diagram.svg diff --git a/resources/icons/editor/diagram.svg b/resources/icons/editor/diagram.svg new file mode 100644 index 000000000..6ac78f56e --- /dev/null +++ b/resources/icons/editor/diagram.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index deb371864..ebc142e2a 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -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'); diff --git a/resources/js/services/drawio.ts b/resources/js/services/drawio.ts index c0a6b5044..4d7d88f1f 100644 --- a/resources/js/services/drawio.ts +++ b/resources/js/services/drawio.ts @@ -127,13 +127,13 @@ export async function show(drawioUrl: string, onInitCallback: () => Promise { +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() { diff --git a/resources/js/wysiwyg-tinymce/plugin-drawio.js b/resources/js/wysiwyg-tinymce/plugin-drawio.js index 3b343a958..342cac0af 100644 --- a/resources/js/wysiwyg-tinymce/plugin-drawio.js +++ b/resources/js/wysiwyg-tinymce/plugin-drawio.js @@ -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; diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 8cbaccd79..0aa04dfd9 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -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 = {}): 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); diff --git a/resources/js/wysiwyg/nodes/diagram.ts b/resources/js/wysiwyg/nodes/diagram.ts index 15726813c..1aff06400 100644 --- a/resources/js/wysiwyg/nodes/diagram.ts +++ b/resources/js/wysiwyg/nodes/diagram.ts @@ -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 { 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 { 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 { + const drawingId = await new Promise((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 { + 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 diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 61b592ca0..e0b58eef6 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -2,9 +2,6 @@ ## In progress -- Add Type: Drawings - - Continue converting drawio to typescript - - Next step to convert http service to ts. ## Main Todo diff --git a/resources/js/wysiwyg/ui/decorators/diagram.ts b/resources/js/wysiwyg/ui/decorators/diagram.ts index 9c48f8c24..0f1263f38 100644 --- a/resources/js/wysiwyg/ui/decorators/diagram.ts +++ b/resources/js/wysiwyg/ui/decorators/diagram.ts @@ -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)); }); }); diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts index bf725f8c8..5316dacf7 100644 --- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts +++ b/resources/js/wysiwyg/ui/defaults/button-definitions.ts @@ -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, diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index 465765caa..22a821a89 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -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; // General user options which may be used by sub elements }; export abstract class EditorUiElement { diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 748370959..31407497f 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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): 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; diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index 9145b8761..f5eae6b21 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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), ]), diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 99045dd5a..b577d1850 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -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 {