assets/app-shell.js
// app-shell.js — the app-inside-the-browser regime. Pages that opt in via a
// [data-shell] container switch from stacked scrolling sections to on-screen
// panes: the site header (the net regime) stays fixed above, the pane nav
// toggles views, and scrolling happens inside the active pane only.
// Without JavaScript nothing happens: the page stays a normal scrolling
// document and the footer stays in place at its bottom.
(function () {
"use strict";
function boot() {
var shell = document.querySelector("[data-shell]");
if (!shell) return;
var nav = shell.querySelector("[data-shell-nav]");
var panes = Array.prototype.slice.call(shell.querySelectorAll("[data-pane]"));
if (!nav || !panes.length) return;
var links = Array.prototype.slice.call(nav.querySelectorAll('a[href^="#"]')).filter(function (a) {
return panes.some(function (p) { return a.hash === "#" + p.id; });
});
if (!links.length) return;
var root = document.documentElement;
var header = document.querySelector("header.chrome") || document.querySelector("header");
var scrolls = new Map();
// The shell hangs from the top of the viewport, below the site header.
var measure = function () {
var h = header ? header.getBoundingClientRect().height : 0;
root.style.setProperty("--app-top", Math.round(h) + "px");
};
measure();
window.addEventListener("resize", measure);
var paneFor = function (id) {
var byId = panes.filter(function (p) { return p.id === id; })[0];
if (byId) return byId;
// Declared hash prefixes (e.g. the drill's "#t/...") route to their host pane.
var head = String(id || "").split("/")[0];
var byPrefix = panes.filter(function (p) {
return (p.getAttribute("data-pane-prefixes") || "").split(",").indexOf(head) > -1;
})[0];
if (byPrefix) return byPrefix;
// Deep links to records, topics, countries: activate the hosting pane.
try {
var host = document.getElementById(id);
while (host && host !== document.body) {
if (host.hasAttribute && host.hasAttribute("data-pane")) return host;
host = host.parentElement;
}
} catch (e) { /* odd ids from location.hash are harmless here */ }
return null;
};
var defaultPane = panes[0].id; // replaced by data-shell-default after definition
var activate = function (id, scroll) {
var target = paneFor(id) || panes.filter(function (p) { return p.id === defaultPane; })[0] || panes[0];
var current = shell.querySelector("[data-pane][data-pane-on]");
if (current) scrolls.set(current.id, current.scrollTop);
panes.forEach(function (p) {
if (p === target) p.setAttribute("data-pane-on", "");
else p.removeAttribute("data-pane-on");
});
links.forEach(function (a) {
if (a.hash === "#" + target.id) a.setAttribute("aria-current", "page");
else a.removeAttribute("aria-current");
});
target.scrollTop = scroll != null ? scroll : (scrolls.get(target.id) || 0);
window.appShellActivePane = target.id;
return target;
};
window.appShellActivate = activate;
// The footer moves into its host pane; without JS it stays at the page bottom.
var footerHost = panes.filter(function (p) { return p.hasAttribute("data-pane-hosts-footer"); })[0];
var footer = document.querySelector("footer.chrome-footer");
if (footerHost && footer) { footerHost.appendChild(footer); footer.setAttribute("data-in-pane", ""); }
nav.addEventListener("click", function (e) {
var a = e.target.closest ? e.target.closest('a[href^="#"]') : null;
if (!a || !panes.some(function (p) { return a.hash === "#" + p.id; })) return;
e.preventDefault();
history.replaceState(null, "", a.hash);
activate(a.hash.slice(1));
});
nav.addEventListener("keydown", function (e) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
var i = links.indexOf(document.activeElement);
if (i === -1) return;
e.preventDefault();
var next = links[(i + (e.key === "ArrowRight" ? 1 : links.length - 1)) % links.length];
next.focus();
history.replaceState(null, "", next.hash);
activate(next.hash.slice(1));
});
var focusDeepLink = function (id, pane) {
if (!pane || id === pane.id) return;
var el = document.getElementById(id);
if (el && el.scrollIntoView) el.scrollIntoView({ block: "start" });
};
window.addEventListener("hashchange", function () {
var id = location.hash.slice(1);
var target = paneFor(id);
if (!target) return;
activate(target.id, id !== target.id ? 0 : undefined);
focusDeepLink(id, target);
});
defaultPane = shell.getAttribute("data-shell-default") || panes[0].id;
if (!panes.some(function (p) { return p.id === defaultPane; })) defaultPane = panes[0].id;
activate.fallback = defaultPane;
root.classList.add("has-shell");
var initialId = location.hash.slice(1);
var initial = paneFor(initialId);
activate(initial ? initial.id : defaultPane);
focusDeepLink(initialId, initial);
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
else boot();
})();