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 {