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";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inline loader for a static app (SVGnest) placed under /public/svgnest.
|
* SVGNest panel – same-origin iframe wrapper.
|
||||||
* It fetches index.html, injects the HTML into a container, then
|
* Renders /public/svgnest/index.html in an isolated doc so the legacy app can
|
||||||
* re-executes <script> tags (both external and inline) in order.
|
* own the DOM/CSS/JS without conflicting with Next.js styles or scripts.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
export default function SVGNestPanel() {
|
export default function SVGNestPanel() {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [ready, setReady] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Basic load/error status for UX
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
const el = iframeRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
async function load() {
|
const onLoad = () => setReady(true);
|
||||||
try {
|
const onError = () => setErr("Failed to load /svgnest/index.html");
|
||||||
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();
|
|
||||||
|
|
||||||
if (cancelled) return;
|
el.addEventListener("load", onLoad);
|
||||||
|
el.addEventListener("error", onError);
|
||||||
// 2) Parse
|
return () => {
|
||||||
const parser = new DOMParser();
|
el.removeEventListener("load", onLoad);
|
||||||
const doc = parser.parseFromString(htmlText, "text/html");
|
el.removeEventListener("error", onError);
|
||||||
|
};
|
||||||
// 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; };
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{error && (
|
{!ready && !err && (
|
||||||
<div className="mb-3 rounded border border-rose-600/40 bg-rose-600/10 p-3 text-sm">
|
<div className="mb-2 text-sm text-zinc-400">Loading SVGnest…</div>
|
||||||
Couldn’t load SVGnest: {error}
|
)}
|
||||||
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{loading && (
|
|
||||||
<div className="mb-3 text-sm text-zinc-400">Loading SVGnest…</div>
|
{/* Same-origin iframe, no sandbox to allow drag/drop, file dialogs, workers, etc. */}
|
||||||
)}
|
<iframe
|
||||||
<div ref={containerRef} className="w-full min-h-[70vh]" />
|
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>
|
</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