Files
WRNexusJS/packages/csr/src/nav-runtime.ts
T
2026-07-12 15:55:18 +05:30

130 lines
5.2 KiB
TypeScript

/**
* Client-side navigation runtime, served at `/__wrnexus/nav.js`.
*
* Progressive enhancement over normal links: intercepts same-origin `<a>`
* clicks, fetches the target page's HTML, swaps the `#app` container in place,
* updates history/title/scroll, ensures any framework runtimes the new page
* needs are present, and re-hydrates. Anything unexpected (cross-origin,
* modified click, non-HTML response, missing `#app`) falls back to a full
* browser navigation, so behaviour degrades safely.
*
* Data "loaders": pages load their data on the server (SSR `api` bindings), so
* the fetched HTML already contains fresh data — no separate client loader is
* needed. Client-side (`csr`) bindings and reactive scopes re-hydrate after the
* swap. Programmatic navigation is exposed as `window.__wrnexusNavigate(url)`.
*/
export const NAV_RUNTIME = String.raw`
(function () {
if (!window.history || !history.pushState || !window.fetch || !window.DOMParser) return;
if (window.__wrnexusNavInstalled) return;
window.__wrnexusNavInstalled = true;
var APP_ID = "app";
function pathOf(src) { return String(src).split("?")[0]; }
function isLocalLink(a) {
if (!a || a.hasAttribute("download") || a.hasAttribute("data-no-nav")) return false;
if (a.target && a.target !== "_self") return false;
if (a.origin !== location.origin) return false;
var href = a.getAttribute("href");
if (!href || href.charAt(0) === "#") return false;
var rel = (a.getAttribute("rel") || "").toLowerCase();
return rel.indexOf("external") === -1;
}
function loadedScriptPaths() {
var set = {};
document.querySelectorAll("script[src]").forEach(function (s) {
var src = s.getAttribute("src");
if (src) set[pathOf(src)] = true;
});
return set;
}
// Append any /__wrnexus/* runtime the incoming page declares but the current
// document has not loaded yet. Fresh scripts self-initialise on load.
function ensureScripts(doc) {
var loaded = loadedScriptPaths();
doc.querySelectorAll("script[src]").forEach(function (s) {
var src = s.getAttribute("src");
if (!src || loaded[pathOf(src)]) return;
loaded[pathOf(src)] = true;
var el = document.createElement("script");
el.src = src;
el.async = false; // preserve execution order (e.g. schemas.js before validate.js)
document.body.appendChild(el);
});
}
// Re-hydrate already-loaded runtimes against the swapped-in DOM. Every
// entrypoint is idempotent, so this is safe even when a fresh script also
// self-initialises the same nodes.
function rehydrate() {
try { if (window.__wrnexusHydrateScopes) window.__wrnexusHydrateScopes(document); } catch (e) {}
try { if (window.__wrnexusHydrateCsrFetches) window.__wrnexusHydrateCsrFetches(document); } catch (e) {}
try { if (window.__wireValidate) window.__wireValidate.init(document); } catch (e) {}
}
function render(html, url, isPop) {
var doc = new DOMParser().parseFromString(html, "text/html");
var to = doc.getElementById(APP_ID);
var from = document.getElementById(APP_ID);
if (!to || !from) { location.href = url; return; } // structure mismatch → hard nav
if (doc.title) document.title = doc.title;
// Swap #app by importing nodes — NOT innerHTML — so it works under a strict
// Trusted-Types CSP (require-trusted-types-for 'script').
var imported = [];
for (var i = 0; i < to.childNodes.length; i++) imported.push(document.importNode(to.childNodes[i], true));
from.replaceChildren.apply(from, imported);
ensureScripts(doc);
rehydrate();
if (!isPop) { history.pushState({ wrnexusNav: true }, "", url); window.scrollTo(0, 0); }
try {
window.dispatchEvent(new CustomEvent("wrnexus:navigated", { detail: { url: url } }));
} catch (e) {}
}
var inFlight = null;
function navigate(url, isPop) {
var token = {};
inFlight = token;
fetch(url, { headers: { "x-wrnexus-nav": "1", accept: "text/html" }, credentials: "same-origin" })
.then(function (r) {
if (inFlight !== token) return null; // superseded by a newer navigation
if (r.redirected && r.url) url = r.url; // follow server redirects (e.g. auth)
var ct = r.headers.get("content-type") || "";
if (ct.indexOf("text/html") === -1) { location.href = url; return null; }
return r.text().then(function (t) {
if (inFlight === token) render(t, url, isPop);
});
})
.catch(function () { location.href = url; });
}
document.addEventListener(
"click",
function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target && e.target.closest ? e.target.closest("a") : null;
if (!isLocalLink(a)) return;
if (a.href === location.href) { e.preventDefault(); return; }
e.preventDefault();
navigate(a.href, false);
},
false,
);
window.addEventListener("popstate", function () {
navigate(location.href, true);
});
// Programmatic navigation for forms/actions and app code.
window.__wrnexusNavigate = function (url) {
navigate(new URL(url, location.href).href, false);
};
})();
`.trim();