--- /dev/null
+<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
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;
}
getDrawIoUrl() {
- // TODO
const drawioUrlElem = document.querySelector('[drawio-url]');
if (drawioUrlElem) {
return drawioUrlElem.getAttribute('drawio-url');
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() {
-import * as DrawIO from '../services/drawio';
+import * as DrawIO from '../services/drawio.ts';
import {wait} from '../services/util';
let pageEditor = null;
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(),
}
});
- const context: EditorUiContext = buildEditorUI(container, editArea, editor);
+ const context: EditorUiContext = buildEditorUI(container, editArea, editor, options);
registerCommonNodeMutationListeners(context);
return new SimpleWysiwygEditorInterface(editor);
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;
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,
};
}
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') || '';
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
## In progress
-- Add Type: Drawings
- - Continue converting drawio to typescript
- - Next step to convert http service to ts.
## Main Todo
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";
setup(context: EditorUiContext, element: HTMLElement) {
const diagramNode = this.getNode();
+ element.classList.add('editor-diagram');
element.addEventListener('click', event => {
context.editor.update(() => {
$selectSingleNode(this.getNode());
element.addEventListener('dblclick', event => {
context.editor.getEditorState().read(() => {
- $openDrawingEditorForNode(context.editor, (this.getNode() as DiagramNode));
+ $openDrawingEditorForNode(context, (this.getNode() as DiagramNode));
});
});
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',
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,
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 {
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,
manager,
translate: (text: string): string => text,
lastSelection: null,
+ options,
};
manager.setContext(context);
// Register context toolbars
manager.registerContextToolbar('image', {
- selector: 'img',
+ selector: 'img:not([drawio-diagram] img)',
content: getImageToolbarContent(),
displayTargetLocator(originalTarget: HTMLElement) {
return originalTarget.closest('a') || originalTarget;
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,
new EditorButton(image),
new EditorButton(horizontalRule),
new EditorButton(codeBlock),
+ new EditorButton(diagram),
new EditorButton(details),
]),
border: 1px dashed var(--editor-color-primary);
}
}
+.editor-diagram.selected {
+ outline: 2px dashed var(--editor-color-primary);
+}
// Editor form elements
.editor-form-field-wrapper {