4d30803b60
Add static site, shared layout, and Node dev server with standard .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.9 KiB
JavaScript
132 lines
3.9 KiB
JavaScript
/**
|
|
* Loads shared nav/footer and applies active menu + copyright year.
|
|
* Requires pages to be served over HTTP (see server.js).
|
|
*/
|
|
(function () {
|
|
var NAV_URL = "/components/nav.html";
|
|
var FOOTER_URL = "/components/footer.html";
|
|
|
|
function normalizePath(pathname) {
|
|
if (!pathname) return "/";
|
|
var p = pathname.split("?")[0].split("#")[0];
|
|
try {
|
|
p = decodeURIComponent(p);
|
|
} catch (e) {
|
|
/* ignore */
|
|
}
|
|
if (p.endsWith("/index.html")) {
|
|
p = p.slice(0, -"/index.html".length) || "/";
|
|
}
|
|
if (p.length > 1 && p.endsWith("/")) {
|
|
p = p.slice(0, -1);
|
|
}
|
|
return p.toLowerCase();
|
|
}
|
|
|
|
function hrefToPath(href) {
|
|
if (!href || href === "#" || href.indexOf("javascript:") === 0) return null;
|
|
try {
|
|
var url = new URL(href, window.location.origin);
|
|
if (url.origin !== window.location.origin) return null;
|
|
return normalizePath(url.pathname);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function pathsMatch(current, target) {
|
|
if (!target) return false;
|
|
if (current === target) return true;
|
|
if (
|
|
(current === "/" || current === "/index.html") &&
|
|
(target === "/" || target === "/index.html")
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function highlightActiveNav(container) {
|
|
var current = normalizePath(window.location.pathname);
|
|
var links = container.querySelectorAll("a[href]");
|
|
|
|
links.forEach(function (anchor) {
|
|
var target = hrefToPath(anchor.getAttribute("href"));
|
|
if (!pathsMatch(current, target)) return;
|
|
|
|
anchor.classList.add("active");
|
|
var item = anchor.closest(".nav-item");
|
|
if (item) item.classList.add("active");
|
|
|
|
if (anchor.classList.contains("dropdown-item")) {
|
|
var parentItem = anchor.closest(".nav-item.dropdown");
|
|
if (parentItem) {
|
|
parentItem.classList.add("active");
|
|
var toggle = parentItem.querySelector(".nav-link.dropdown-toggle");
|
|
if (toggle) toggle.classList.add("active");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCopyrightYear(root) {
|
|
var year = String(new Date().getFullYear());
|
|
root.querySelectorAll("#current-year").forEach(function (el) {
|
|
el.textContent = year;
|
|
});
|
|
}
|
|
|
|
function fetchText(url) {
|
|
return fetch(url, { credentials: "same-origin" }).then(function (res) {
|
|
if (!res.ok) throw new Error("Failed to load " + url + " (" + res.status + ")");
|
|
return res.text();
|
|
});
|
|
}
|
|
|
|
function injectHtml(containerId, html) {
|
|
var el = document.getElementById(containerId);
|
|
if (!el) return null;
|
|
el.innerHTML = html;
|
|
return el;
|
|
}
|
|
|
|
function initLayout() {
|
|
var navContainer = document.getElementById("nav-container");
|
|
var footerContainer = document.getElementById("footer-container");
|
|
if (!navContainer && !footerContainer) return;
|
|
|
|
var tasks = [];
|
|
if (navContainer) tasks.push(fetchText(NAV_URL));
|
|
else tasks.push(Promise.resolve(null));
|
|
if (footerContainer) tasks.push(fetchText(FOOTER_URL));
|
|
else tasks.push(Promise.resolve(null));
|
|
|
|
Promise.all(tasks)
|
|
.then(function (parts) {
|
|
var navHtml = parts[0];
|
|
var footerHtml = parts[1];
|
|
if (navHtml) {
|
|
var navRoot = injectHtml("nav-container", navHtml);
|
|
if (navRoot) highlightActiveNav(navRoot);
|
|
}
|
|
if (footerHtml) {
|
|
var footerRoot = injectHtml("footer-container", footerHtml);
|
|
if (footerRoot) updateCopyrightYear(footerRoot);
|
|
}
|
|
if (navHtml && window.jQuery && window.jQuery.fn.dropdown) {
|
|
window.jQuery("#nav-container [data-toggle=\"dropdown\"]").dropdown();
|
|
}
|
|
document.dispatchEvent(new CustomEvent("aifo:layout-ready"));
|
|
})
|
|
.catch(function (err) {
|
|
console.error("[layout.js]", err);
|
|
});
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", initLayout);
|
|
} else {
|
|
initLayout();
|
|
}
|
|
})();
|