BookStack/resources/js/wysiwyg/nodes/diagram.ts
Dan Brown c7c0df0964
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...)
2024-07-19 12:09:41 +01:00

219 lines
6.2 KiB
TypeScript

import {
DecoratorNode,
DOMConversion,
DOMConversionMap,
DOMConversionOutput,
LexicalEditor, LexicalNode,
SerializedLexicalNode,
Spread
} from "lexical";
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;
drawingId: string;
drawingUrl: string;
}, SerializedLexicalNode>
export class DiagramNode extends DecoratorNode<EditorDecoratorAdapter> {
__id: string = '';
__drawingId: string = '';
__drawingUrl: string = '';
static getType(): string {
return 'diagram';
}
static clone(node: DiagramNode): DiagramNode {
return new DiagramNode(node.__drawingId, node.__drawingUrl);
}
constructor(drawingId: string, drawingUrl: string, key?: string) {
super(key);
this.__drawingId = drawingId;
this.__drawingUrl = drawingUrl;
}
setDrawingIdAndUrl(drawingId: string, drawingUrl: string): void {
const self = this.getWritable();
self.__drawingUrl = drawingUrl;
self.__drawingId = drawingId;
}
getDrawingIdAndUrl(): { id: string, url: string } {
const self = this.getLatest();
return {
id: self.__drawingId,
url: self.__drawingUrl,
};
}
setId(id: string) {
const self = this.getWritable();
self.__id = id;
}
getId(): string {
const self = this.getLatest();
return self.__id;
}
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
return {
type: 'diagram',
getNode: () => this,
};
}
isInline(): boolean {
return false;
}
isIsolated() {
return true;
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
return el('div', {
id: this.__id || null,
'drawio-diagram': this.__drawingId,
}, [
el('img', {src: this.__drawingUrl}),
]);
}
updateDOM(prevNode: DiagramNode, dom: HTMLElement) {
const img = dom.querySelector('img');
if (!img) return false;
if (prevNode.__id !== this.__id) {
dom.setAttribute('id', this.__id);
}
if (prevNode.__drawingUrl !== this.__drawingUrl) {
img.setAttribute('src', this.__drawingUrl);
}
if (prevNode.__drawingId !== this.__drawingId) {
dom.setAttribute('drawio-diagram', this.__drawingId);
}
return false;
}
static importDOM(): DOMConversionMap | null {
return {
div(node: HTMLElement): DOMConversion | null {
if (!node.hasAttribute('drawio-diagram')) {
return null;
}
return {
conversion: (element: HTMLElement): DOMConversionOutput | null => {
const img = element.querySelector('img');
const drawingUrl = img?.getAttribute('src') || '';
const drawingId = element.getAttribute('drawio-diagram') || '';
return {
node: $createDiagramNode(drawingId, drawingUrl),
};
},
priority: 3,
};
},
};
}
exportJSON(): SerializedDiagramNode {
return {
type: 'diagram',
version: 1,
id: this.__id,
drawingId: this.__drawingId,
drawingUrl: this.__drawingUrl,
};
}
static importJSON(serializedNode: SerializedDiagramNode): DiagramNode {
const node = $createDiagramNode(serializedNode.drawingId, serializedNode.drawingUrl);
node.setId(serializedNode.id || '');
return node;
}
}
export function $createDiagramNode(drawingId: string = '', drawingUrl: string = ''): DiagramNode {
return new DiagramNode(drawingId, drawingUrl);
}
export function $isDiagramNode(node: LexicalNode | null | undefined) {
return node instanceof DiagramNode;
}
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);
});
}