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 {