All checks were successful
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 4m9s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m36s
CI / Import/export fidelity gate (push) Successful in 47s
A new block plugin bundling the OFFICIAL draw.io editor — nothing ever
loads from diagrams.net; the sandbox CSP pins every request to the
plugin's own version-pinned asset path (zero-external-network verified
live via a request-capture run).
Plugin (packages/plugins/drawio):
- block data { xml, svg }: xml is the draw.io source (document of
record), svg the rendered snapshot as raw markup — render mode,
office/PDF exports (the existing fallback renderer already inlines
data.svg) and the public view all show the diagram without running
diagram code
- edit mode: snapshot + "edit in fullscreen" (an empty block opens the
editor immediately); the bundled editor runs in a child iframe of the
plugin's own assets and speaks draw.io's JSON embed protocol —
Save & Exit exports xmlsvg, persists { xml, svg } via blockData, and
drops back to the inline size
- build.mjs fetches the pinned release (v30.3.6) into a gitignored
vendor/ cache (fonts-build pattern; skipped in CI — plugin.js still
bundles, the installable ZIP needs a dev machine) and packs a trimmed
webapp subset: no dev sources, no embed.diagrams.net integrations
bundle, no standalone viewers, no MathJax/templates/PWA — 27 MiB ZIP,
85 MiB unpacked, de+en editor languages
Host/SDK extensions (generic, not drawio-specific):
- new ui.enterFullscreen()/exitFullscreen(): the surface's frame becomes
a viewport-covering overlay — same sandboxed iframe, only geometry
changes; destroy removes the frame, so a vanished plugin can never
leave the app covered
- sandbox CSP: connect-src/frame-src now allow the plugin's OWN asset
path (was 'none') — bundled apps lazy-load their resources and run in
a child frame, but the api and external hosts stay unreachable; HTML
assets are served with the same CSP so a packaged page cannot widen
the rules, and child frames inherit the sandbox attribute
- plugin size limits raised (ZIP 5→64 MiB, unpacked 20→256 MiB) for
bundled-app plugins; content types for xml/txt/ico assets
Verified end to end against a local stack (9/9): install via dropzone
(85 MiB validation), block insert, fullscreen entry, bundled editor
boots inside the double sandbox (German UI), shape drawn, Save & Exit
persists, snapshot renders inline, survives reload, zero off-origin
requests throughout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
220 lines
7.7 KiB
TypeScript
220 lines
7.7 KiB
TypeScript
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<string, Record<string, string>> = { 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<void> {
|
|
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);
|
|
}
|