Files

102 lines
4.2 KiB
JavaScript

/* pjax — client-side navigation. swaps the page content instead of a full
reload, so the cursor, shell and theme stay alive (no flashing, no re-fetch
of css/js). pure progressive enhancement: if fetch/history/DOMParser are
missing, or anything goes wrong, links fall back to normal navigation. */
(function () {
"use strict";
if (!window.history || !window.fetch || !window.DOMParser) return;
var MAIN = "#main-content";
if (!document.querySelector(MAIN)) return;
function internal(a) {
return a && a.href && a.origin === location.origin &&
a.getAttribute("href").charAt(0) !== "#" &&
!a.hasAttribute("download") &&
(a.getAttribute("target") || "") !== "_blank";
}
function swap(html, url) {
var doc = new DOMParser().parseFromString(html, "text/html");
var nm = doc.querySelector(MAIN), om = document.querySelector(MAIN);
if (!nm || !om) { location.href = url; return; }
om.innerHTML = nm.innerHTML;
if (doc.title) document.title = doc.title;
document.body.className = doc.body.className;
// breadcrumb (in the persistent header) — swap its inner links
var np = doc.querySelector(".prompt__path"), op = document.querySelector(".prompt__path");
if (np && op) op.innerHTML = np.innerHTML;
// per-section typeable routes live in .topbar — replace them
var topbar = document.querySelector(".topbar");
// language switch (persistent header) — repoint it at THIS page's
// translation, otherwise it keeps the target of the first-loaded page
var nl = doc.querySelector(".lang-switch"), ol = document.querySelector(".lang-switch");
if (ol && ol.parentNode) ol.parentNode.removeChild(ol);
if (nl && topbar) topbar.insertBefore(document.importNode(nl, true), topbar.firstChild);
var oldR = document.querySelector(".routes");
if (oldR && oldR.parentNode) oldR.parentNode.removeChild(oldR);
var newR = doc.querySelector(".routes");
if (newR && topbar) topbar.appendChild(document.importNode(newR, true));
window.scrollTo(0, 0);
document.dispatchEvent(new CustomEvent("pjax:load"));
}
// prefetch cache: hovering a link fetches its HTML so the click is instant
var cache = {};
function fetchHTML(url) {
return fetch(url, { credentials: "same-origin" })
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); });
}
function prefetch(url) {
if (!cache[url]) cache[url] = fetchHTML(url).catch(function () { delete cache[url]; return null; });
}
// which language a URL belongs to (de lives under /de/, en is the default)
function langOf(u) {
var p;
try { p = new URL(u, location.origin).pathname; } catch (e) { p = u; }
return /^\/de(\/|$)/.test(p) ? "de" : "en";
}
var busy = false;
function go(url, push) {
if (busy) return;
// a language change rebuilds the whole chrome (menu, html lang, switch),
// so do a real reload instead of a partial content swap
if (langOf(url) !== (document.documentElement.lang || "en")) { location.href = url; return; }
busy = true;
(cache[url] || fetchHTML(url))
.then(function (html) {
busy = false;
if (!html) { location.href = url; return; } // prefetch had failed
if (push) history.pushState({ pjax: true }, "", url);
swap(html, url);
})
.catch(function () { busy = false; location.href = url; });
}
document.addEventListener("mouseover", function (e) {
var a = e.target.closest ? e.target.closest("a[href]") : null;
if (internal(a) && a.href.split("#")[0] !== location.href.split("#")[0]) prefetch(a.href);
}, { passive: true });
document.addEventListener("click", function (e) {
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
var a = e.target.closest ? e.target.closest("a[href]") : null;
if (!internal(a)) return;
if (a.href.split("#")[0] === location.href.split("#")[0]) return; // same document
e.preventDefault();
go(a.href, true);
});
window.addEventListener("popstate", function () { go(location.href, false); });
history.replaceState({ pjax: true }, "", location.href);
// bridge for programmatic navigation (shell commands, filebrowser Enter)
window.pjax = function (url) { go(url, true); };
})();