1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
/**
* DrawioView renders a .drawio / .dio diagram with the diagrams.net embed
* viewer, loaded in an iframe and fed the XML over its postMessage protocol
* (`init` → `load`). There is no offline renderer for draw.io's shape
* library that ori could bundle, so an isolated sandbox without access to
* embed.diagrams.net gets a notice and the XML source instead.
*
* @example
* <DrawioView xml={content} onUnavailable={() => setMode("source")} />
*/
import { useEffect, useRef, useState } from "react";
/** Read-only embed: lightbox chrome (zoom, pages, layers), no editor UI. */
export const DRAWIO_EMBED_URL =
"https://embed.diagrams.net/?embed=1&proto=json&spin=1&lightbox=1&nav=1&chrome=0";
/** How long to wait for the viewer's `init` before declaring it unreachable. */
const DEFAULT_TIMEOUT_MS = 8000;
interface DrawioViewProps {
xml: string;
/** Override for tests; defaults to DRAWIO_EMBED_URL. */
embedUrl?: string;
/** Override for tests; defaults to 8 s. */
timeoutMs?: number;
}
type ViewerStatus = "loading" | "ready" | "unavailable";
export function DrawioView({
xml,
embedUrl = DRAWIO_EMBED_URL,
timeoutMs = DEFAULT_TIMEOUT_MS,
}: DrawioViewProps) {
const frameRef = useRef<HTMLIFrameElement>(null);
const [status, setStatus] = useState<ViewerStatus>("loading");
useEffect(() => {
const frame = frameRef.current;
if (!frame) {
return;
}
const timer = setTimeout(() => {
setStatus((current) => (current === "ready" ? current : "unavailable"));
}, timeoutMs);
const onMessage = (event: MessageEvent) => {
if (event.source !== frame.contentWindow) {
return;
}
const message = parseViewerMessage(event.data);
if (message?.event === "init") {
frame.contentWindow?.postMessage(
JSON.stringify({ action: "load", xml, autosave: 0 }),
"*",
);
}
if (message?.event === "init" || message?.event === "load") {
clearTimeout(timer);
setStatus("ready");
}
};
window.addEventListener("message", onMessage);
return () => {
clearTimeout(timer);
window.removeEventListener("message", onMessage);
};
}, [xml, timeoutMs]);
return (
<div className={`drawio-view drawio-view-${status}`}>
{status === "unavailable" && (
<p className="drawio-notice">
The diagram viewer (embed.diagrams.net) is not reachable from this
browser; use the <strong>Source</strong> view to read the diagram XML.
</p>
)}
{status === "loading" && <p className="pane-empty">loading viewer…</p>}
<iframe
ref={frameRef}
className="drawio-frame"
title="draw.io diagram"
src={embedUrl}
sandbox="allow-scripts allow-same-origin allow-popups"
hidden={status === "unavailable"}
/>
</div>
);
}
/** parseViewerMessage decodes one embed-protocol frame; anything else is null. */
function parseViewerMessage(data: unknown): { event?: string } | null {
if (typeof data !== "string" || !data.startsWith("{")) {
return null;
}
try {
return JSON.parse(data) as { event?: string };
} catch {
return null;
}
}
|