source.fact.ngo a coherence.ngo project

assets/app-shell.js

raw ↗ · AGPL-3.0

// 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(); })();