// Site-wide htmx policy. Loaded right after htmx itself. (function () { // htmx wires up swapped-in content (e.g. a dialog's hx-post form) in its // settle step, by default 20ms after the swap: a click or Enter in that // window would submit the form natively. No settle transitions are used, // so settle in the same tick. htmx.config.defaultSettleDelay = 0; function boosting() { return document.body.getAttribute('hx-boost') === 'true'; } // A server-sent HX-Redirect follows a mutation (move, delete, …): load the // target into the current history entry rather than pushing a new one, so // Back skips the stale pre-mutation page. Boosted pages swap it in like a // navigation; elsewhere replaceState + reload (not location.replace: a // target differing only by #fragment would otherwise not re-fetch). htmx.location = { set href(target) { if (boosting()) { if (window.closeModal) closeModal(); htmx.ajax('GET', target, { target: '.shell', swap: 'innerHTML transition:true', headers: { 'HX-Boosted': 'true' }, replace: target }); return; } window.history.replaceState(null, '', target); window.location.reload(); }, reload: function () { window.location.reload(); } }; // Failed requests answer with a plain-text reason; show it. A failed // boosted navigation falls back to loading the page normally instead. function fallBack(e) { if (!e.detail.boosted) return false; window.location.href = e.detail.pathInfo.finalRequestPath || e.detail.pathInfo.requestPath; return true; } document.addEventListener('htmx:responseError', function (e) { if (fallBack(e)) return; var xhr = e.detail.xhr; alert(xhr.responseText || ('Request failed (' + xhr.status + ')')); }); document.addEventListener('htmx:sendError', function (e) { if (fallBack(e)) return; alert('Network error'); }); // --- Boosted navigation --------------------------------------------- // Outside the editor, links and GET forms to wiki pages load the next page // into .shell instead of replacing the document: the server answers with // the "partial" (title, page actions out-of-band, shell content). The // header stays put, nothing flashes in browsers without cross-document // view transitions, and the swap runs as a same-document view transition. // Back/Forward refetch the partial (hx-history="false" on .shell: no // snapshots that could show a page from before a move or delete). htmx.config.scrollIntoViewOnBoost = false; // Only wiki pages are boosted: same origin, a slash-terminated path, no // query except a search (?q=). Files, the editor (?edit), internal // endpoints (/_…) and POST forms navigate normally. function isPageURL(url) { if (url.origin !== location.origin) return false; if (!url.pathname.endsWith('/') || url.pathname.indexOf('/_') === 0) return false; var ok = true; url.searchParams.forEach(function (v, key) { if (key !== 'q') ok = false; }); return ok; } // htmx decides per element whether it is boosted, right after this event. document.addEventListener('htmx:beforeProcessNode', function (e) { var elt = e.target; if (elt.hasAttribute('hx-boost')) return; var page = false; if (elt.tagName === 'A') { page = elt.hasAttribute('href') && !elt.hasAttribute('download') && isPageURL(new URL(elt.href)); } else if (elt.tagName === 'FORM') { page = (elt.getAttribute('method') || 'get').toLowerCase() === 'get' && isPageURL(new URL(elt.action)); } else { return; } if (!page) elt.setAttribute('hx-boost', 'false'); }); function isShell(elt) { return elt && elt.classList && elt.classList.contains('shell'); } // Scroll positions: the tree rail keeps its position across navigation; // the content column (.center) starts at the top, or where it was when // coming back via Back/Forward. var treeTop = 0; var restoring = false; var centerTops = {}; function pageKey() { return location.pathname + location.search; } function rememberTree() { var tree = document.querySelector('.tree-scroll'); treeTop = tree ? tree.scrollTop : 0; } document.addEventListener('scroll', function (e) { var t = e.target; if (t.classList && t.classList.contains('center')) centerTops[pageKey()] = t.scrollTop; }, { capture: true, passive: true }); document.addEventListener('htmx:beforeSwap', function (e) { if (e.detail.boosted) { e.detail.target = document.querySelector('.shell'); e.detail.swapOverride = 'innerHTML transition:true'; } if (isShell(e.detail.target)) rememberTree(); }); document.addEventListener('htmx:historyCacheMissLoad', function () { restoring = true; rememberTree(); }); // After a new page is in .shell: restore scroll positions, then tell page // scripts (companion.js, …) to apply themselves to the new content. document.addEventListener('htmx:afterSettle', function (e) { if (!isShell(e.target)) return; var tree = document.querySelector('.tree-scroll'); if (tree) { tree.scrollTop = treeTop; var active = tree.querySelector('.tree-row.is-active'); if (active) active.scrollIntoView({ block: 'nearest' }); } if (restoring) { restoring = false; var center = document.querySelector('.center'); if (center) center.scrollTop = centerTops[pageKey()] || 0; } document.dispatchEvent(new CustomEvent('page:swapped')); }); })();