nandi/oripublic Fork 0
b6cd929ab1425a4ef6073ea9c189917cca02cf3c
Commits
Clone
git clone https://git.rickub.com/nandi/ori.git
git clone ssh://git@rickub.com/nandi/ori.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

forked from bots-garden/ori

DrawioView.tsx · 102 lines · 3.0 KBTypeScript Blame HistoryRaw
  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;
	}
}