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