svgnest render fix
This commit is contained in:
parent
90cab6f8b2
commit
13879407f7
1 changed files with 49 additions and 129 deletions
|
|
@ -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">
|
||||
Couldn’t 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">
|
||||
Couldn’t 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 doesn’t 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue