import { createPlugin, windowTransport, type RenderContext } from '@dorfteich/plugin-sdk'; import de from '../i18n/de.json'; import en from '../i18n/en.json'; /** * draw.io reference plugin: diagrams edited in the REAL draw.io editor, * which the package bundles as static assets — nothing is ever loaded from * diagrams.net; the sandbox CSP pins every request to the plugin's own * version-pinned asset path. * * Block data: `{ xml, svg }` — `xml` is the draw.io source (document of * record), `svg` the rendered snapshot as raw markup, so render mode, * office/PDF exports (fallback renderer) and the public read view can all * show the diagram without running any diagram code. * * Render mode shows the snapshot. Edit mode shows the snapshot plus an * "edit in fullscreen" button (an empty block opens the editor * immediately): the plugin asks the host for a viewport-covering frame * (`ui.enterFullscreen`), boots the bundled editor in a child iframe of * its own assets, and speaks draw.io's JSON embed protocol with it — * Save & Exit exports the SVG, persists `{ xml, svg }` through * `blockData.setData`, and returns the frame to its inline size. */ const STRINGS: Record> = { de, en }; function labelFor(locale: string, key: string): string { const base = locale.split('-')[0] ?? locale; return STRINGS[base]?.[key] ?? STRINGS.en?.[key] ?? key; } interface DiagramData { xml?: string; svg?: string; } function dataOf(context: RenderContext): DiagramData { return context.data && typeof context.data === 'object' ? (context.data as DiagramData) : {}; } const { host } = createPlugin({ transport: windowTransport({ target: { postMessage: (message) => window.parent.postMessage(message, '*') }, source: window, }), onRender: (context) => renderMode(context), onEdit: (context) => editMode(context), onDestroy: () => closeEditor(), }); function resize(): void { void host.ui.resize(Math.max(64, document.body.scrollHeight + 16)); } /** The stored snapshot as an inline drawing, or the empty-state hint. */ function snapshotElement(data: DiagramData, locale: string): HTMLElement { if (data.svg && data.svg.trim() !== '') { const holder = document.createElement('div'); holder.className = 'dt-drawio__snapshot'; holder.innerHTML = data.svg; const svg = holder.querySelector('svg'); // The export SVG carries fixed pixel dimensions; scale down to fit. if (svg) { svg.style.maxWidth = '100%'; svg.style.height = 'auto'; } return holder; } const hint = document.createElement('p'); hint.textContent = labelFor(locale, 'empty'); hint.style.color = '#6b7280'; return hint; } function renderMode(context: RenderContext): void { closeEditor(); const data = dataOf(context); document.body.textContent = ''; document.body.className = 'dt-drawio dt-drawio--render'; document.body.appendChild(snapshotElement(data, context.locale)); resize(); } function editMode(context: RenderContext): void { closeEditor(); const data = dataOf(context); document.body.textContent = ''; document.body.className = 'dt-drawio dt-drawio--edit'; document.body.appendChild(snapshotElement(data, context.locale)); const button = document.createElement('button'); button.type = 'button'; button.textContent = labelFor(context.locale, data.svg ? 'editButton' : 'createButton'); button.style.cssText = 'display:block;margin:8px 0;padding:6px 12px;cursor:pointer;font:inherit;'; button.addEventListener('click', () => void openEditor(context)); document.body.appendChild(button); resize(); // A block that has no diagram yet goes straight into the editor — the // user just inserted it and clearly wants to draw. if (!data.svg && !data.xml) void openEditor(context); } /** The active fullscreen editing session, if any. */ let session: { frame: HTMLIFrameElement; onMessage: (event: MessageEvent) => void } | null = null; function closeEditor(): void { if (!session) return; window.removeEventListener('message', session.onMessage); session.frame.remove(); session = null; void host.ui.exitFullscreen(); } async function openEditor(context: RenderContext): Promise { if (session) return; const data = dataOf(context); const locale = context.locale.split('-')[0] ?? 'en'; await host.ui.enterFullscreen(); const frame = document.createElement('iframe'); frame.className = 'dt-drawio__editor'; frame.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;border:none;background:#fff;'; frame.title = 'draw.io'; // The bundled editor, resolved relative to this frame's document — i.e. // from the same version-pinned plugin asset path the CSP allows. const params = new URLSearchParams({ embed: '1', proto: 'json', spin: '1', libraries: '1', saveAndExit: '1', noSaveBtn: '1', lang: locale, }); frame.src = `./assets/drawio/index.html?${params.toString()}`; let latestXml = data.xml ?? ''; const post = (message: object): void => { frame.contentWindow?.postMessage(JSON.stringify(message), '*'); }; const onMessage = (event: MessageEvent): void => { if (event.source !== frame.contentWindow || typeof event.data !== 'string') return; let message: { event?: string; xml?: string; data?: string }; try { message = JSON.parse(event.data) as typeof message; } catch { return; } switch (message.event) { case 'init': post({ action: 'load', xml: latestXml, autosave: 0 }); break; case 'save': // Save (& Exit): remember the source, then ask for the SVG render — // the pair is persisted together when the export answer arrives. latestXml = message.xml ?? latestXml; post({ action: 'export', format: 'xmlsvg' }); break; case 'export': { const svg = decodeSvgDataUri(message.data ?? ''); latestXml = message.xml ?? latestXml; void host.blockData.setData({ xml: latestXml, svg } satisfies DiagramData).then(() => { closeEditor(); finishEdit({ xml: latestXml, svg }, context.locale); }); break; } case 'exit': closeEditor(); finishEdit(dataOf(context), context.locale); break; default: break; } }; window.addEventListener('message', onMessage); session = { frame, onMessage }; document.body.appendChild(frame); } /** Redraws the inline edit surface after the fullscreen editor closed. */ function finishEdit(data: DiagramData, locale: string): void { document.body.textContent = ''; document.body.className = 'dt-drawio dt-drawio--edit'; document.body.appendChild(snapshotElement(data, locale)); const button = document.createElement('button'); button.type = 'button'; button.textContent = labelFor(locale, data.svg ? 'editButton' : 'createButton'); button.style.cssText = 'display:block;margin:8px 0;padding:6px 12px;cursor:pointer;font:inherit;'; button.addEventListener( 'click', () => void openEditor({ extensionPointId: 'diagram', locale, data }), ); document.body.appendChild(button); if (data.svg) { const note = document.createElement('p'); note.textContent = labelFor(locale, 'saved'); note.style.cssText = 'color:#6b7280;font-size:13px;'; document.body.appendChild(note); } resize(); } /** draw.io's `xmlsvg` export arrives as a base64 data URI; the block stores * raw SVG markup (like the mermaid plugin) so the api-side fallback renderer * can sanitize and inline it into exports and the public read view. */ function decodeSvgDataUri(dataUri: string): string { const base64 = dataUri.split(',')[1] ?? ''; const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)); return new TextDecoder().decode(bytes); }