svgnest render fix

This commit is contained in:
makearmy 2025-10-15 20:45:56 -04:00
parent 90cab6f8b2
commit 13879407f7

View file

@ -3,147 +3,67 @@
import { useEffect, useRef, useState } from "react";
/**
* Inline loader for a static app (SVGnest) placed under /public/svgnest.
* It fetches index.html, injects the HTML into a container, then
* re-executes <script> tags (both external and inline) in order.
* CSS <link> files are fetched and inlined into <style> tags to avoid path issues.
*
* NOTE: This runs in the light DOM (not shadow). That maximizes compatibility
* with older scripts that rely on document-level selectors. If we see style bleed,
* we can optionally switch to a Shadow DOM variant later.
* SVGNest panel same-origin iframe wrapper.
* Renders /public/svgnest/index.html in an isolated doc so the legacy app can
* own the DOM/CSS/JS without conflicting with Next.js styles or scripts.
*/
export default function SVGNestPanel() {
const containerRef = useRef<HTMLDivElement | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [ready, setReady] = useState(false);
const [err, setErr] = useState<string | null>(null);
// Basic load/error status for UX
useEffect(() => {
let cancelled = false;
const el = iframeRef.current;
if (!el) return;
async function load() {
try {
setLoading(true);
// 1) Fetch index.html
const htmlRes = await fetch("/svgnest/index.html", { cache: "no-cache" });
if (!htmlRes.ok) throw new Error(`Failed to fetch index.html (HTTP ${htmlRes.status})`);
const htmlText = await htmlRes.text();
const onLoad = () => setReady(true);
const onError = () => setErr("Failed to load /svgnest/index.html");
if (cancelled) return;
// 2) Parse
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, "text/html");
// 3) Find and inline CSS (convert <link rel="stylesheet"> to <style>)
const linkEls = Array.from(doc.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"][href]'));
for (const link of linkEls) {
const href = link.getAttribute("href") || "";
const absHref = absolutizeAsset(href);
try {
const cssRes = await fetch(absHref, { cache: "no-cache" });
if (cssRes.ok) {
const cssText = await cssRes.text();
const style = doc.createElement("style");
style.setAttribute("data-inlined-from", href);
style.textContent = cssText;
link.replaceWith(style);
} else {
// leave the original link as a fallback
}
} catch {
// leave the original link as a fallback
}
}
// 4) Extract scripts (to re-execute later) & remove them from doc body
const scriptInfos: { src?: string; inline?: string; type?: string; nomodule?: boolean; async?: boolean; defer?: boolean }[] = [];
doc.querySelectorAll("script").forEach((s) => {
scriptInfos.push({
src: s.getAttribute("src") || undefined,
inline: s.textContent || undefined,
type: s.getAttribute("type") || undefined,
nomodule: s.hasAttribute("nomodule"),
async: s.hasAttribute("async"),
defer: s.hasAttribute("defer"),
});
s.remove(); // prevent duplicate execution when we inject innerHTML
});
// 5) Inject BODY HTML into our container
const host = containerRef.current!;
host.innerHTML = doc.body.innerHTML;
// 6) Sequentially (re)execute scripts in original order
// We do this after body is in place so DOM targets exist.
for (const info of scriptInfos) {
await runScript(host, info);
}
} catch (e: any) {
if (!cancelled) setError(e?.message || String(e));
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
el.addEventListener("load", onLoad);
el.addEventListener("error", onError);
return () => {
el.removeEventListener("load", onLoad);
el.removeEventListener("error", onError);
};
}, []);
return (
<div className="w-full">
{error && (
<div className="mb-3 rounded border border-rose-600/40 bg-rose-600/10 p-3 text-sm">
Couldnt load SVGnest: {error}
{!ready && !err && (
<div className="mb-2 text-sm text-zinc-400">Loading SVGnest</div>
)}
{err && (
<div className="mb-2 rounded border border-rose-600/40 bg-rose-600/10 p-3 text-sm">
Couldnt load SVGnest: {err}
</div>
)}
{loading && (
<div className="mb-3 text-sm text-zinc-400">Loading SVGnest</div>
)}
<div ref={containerRef} className="w-full min-h-[70vh]" />
{/* Same-origin iframe, no sandbox to allow drag/drop, file dialogs, workers, etc. */}
<iframe
ref={iframeRef}
src="/svgnest/index.html"
title="SVGNest"
className="w-full rounded-md border"
style={{
height: "72vh",
background: "transparent",
}}
/>
{/* Fallback open-in-new-tab helper */}
<div className="mt-2 text-xs text-zinc-400">
If the app doesnt respond,{" "}
<a
href="/svgnest/index.html"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
open SVGnest in a new tab
</a>
.
</div>
</div>
);
}
/** Convert a (possibly relative) asset path from /svgnest/index.html into an absolute path */
function absolutizeAsset(path: string): string {
// Ignore full URLs
try {
const u = new URL(path);
return u.toString();
} catch {
// not a full URL
}
// Handle protocol-relative //host/path -> treat as absolute
if (path.startsWith("//")) return window.location.protocol + path;
// If already absolute (/x/y), leave as-is
if (path.startsWith("/")) return path;
// Otherwise treat as relative to /svgnest/
return `/svgnest/${path.replace(/^\.?\//, "")}`;
}
/** Append a new <script> to the container, respecting order. Supports src and inline. */
function runScript(container: HTMLElement, info: { src?: string; inline?: string; type?: string; nomodule?: boolean; async?: boolean; defer?: boolean; }): Promise<void> {
return new Promise((resolve, reject) => {
const s = document.createElement("script");
if (info.type) s.type = info.type;
if (info.nomodule) s.noModule = true;
// Preserve async/defer semantics if they existed
if (info.async) s.async = true;
if (info.defer) s.defer = true;
if (info.src) {
s.src = absolutizeAsset(info.src);
s.onload = () => resolve();
s.onerror = () => reject(new Error(`Failed to load script: ${s.src}`));
container.appendChild(s);
} else {
// Inline script: execute synchronously
s.textContent = info.inline || "";
container.appendChild(s);
resolve();
}
});
}