Files

241 lines
7.5 KiB
JavaScript
Raw Permalink Normal View History

(() => {
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
function isPlainLeftClick(event) {
return (
event.button === 0 &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey
);
}
function setupFileTabs() {
const index = $("[data-file-index]");
if (!index) return;
const tabs = $$("[data-file-tab]", index);
const panels = $$("[data-file-panel]", index);
const announcer = $("[data-file-announcer]", index);
const canonical = $('link[rel="canonical"]');
const routes = new Map(
tabs.map((tab) => [new URL(tab.href, window.location.href).pathname, tab.dataset.fileTab])
);
let activeId = null;
function idForLocation() {
return routes.get(window.location.pathname) || "posts";
}
function activate(id, { announce = false } = {}) {
const activeTab = tabs.find((tab) => tab.dataset.fileTab === id) || tabs[0];
if (!activeTab) return;
const changed = activeId !== activeTab.dataset.fileTab;
activeId = activeTab.dataset.fileTab;
for (const tab of tabs) {
const active = tab === activeTab;
tab.classList.toggle("active", active);
if (active) tab.setAttribute("aria-current", "page");
else tab.removeAttribute("aria-current");
}
for (const panel of panels) {
panel.hidden = panel.dataset.filePanel !== activeTab.dataset.fileTab;
}
document.title = activeTab.dataset.pageTitle || document.title;
if (canonical) canonical.href = new URL(activeTab.href, window.location.href).href;
if (announce && changed && announcer) {
announcer.textContent = `Showing ${activeTab.textContent.trim()}`;
}
}
for (const tab of tabs) {
tab.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
const url = new URL(tab.href, window.location.href);
const nextLocation = `${url.pathname}${url.search}${url.hash}`;
const currentLocation = `${window.location.pathname}${window.location.search}${window.location.hash}`;
if (nextLocation !== currentLocation) {
window.history.pushState({ fileTab: tab.dataset.fileTab }, "", nextLocation);
}
activate(tab.dataset.fileTab, { announce: true });
window.dispatchEvent(new CustomEvent("filetabchange"));
});
}
window.addEventListener("popstate", () => activate(idForLocation(), { announce: true }));
activate(idForLocation());
}
function setupToc() {
const headings = $$(".post-content h2[id], .post-content h3[id]");
const links = $$("#TableOfContents a");
if (!headings.length || !links.length) return;
let queued = false;
function update() {
const y = window.scrollY + window.innerHeight * 0.35;
let activeId = headings[0].id;
for (const heading of headings) {
const headingY = heading.getBoundingClientRect().top + window.scrollY;
if (headingY > y) break;
activeId = heading.id;
}
for (const link of links) {
link.classList.toggle(
"active",
decodeURIComponent(link.hash.slice(1)) === activeId
);
}
queued = false;
}
function queue() {
if (queued) return;
queued = true;
window.requestAnimationFrame(update);
}
window.addEventListener("scroll", queue, { passive: true });
window.addEventListener("resize", queue);
update();
}
function setupPostTags() {
const list = $("[data-post-list]");
if (!list) return;
const rows = $$(".post-row", list);
const links = $$("[data-tag]");
const status = $("[data-tag-filter-status]");
const clear = status?.querySelector("a");
const empty = $("[data-tag-empty]");
function setUrl(tag) {
const url = new URL(window.location.href);
if (tag) url.searchParams.set("tag", tag);
else url.searchParams.delete("tag");
window.history.pushState({ tag }, "", `${url.pathname}${url.search}`);
}
function applyTag(tag) {
for (const link of links) {
link.classList.toggle("active", link.dataset.tag === tag);
}
let visible = 0;
for (const row of rows) {
const tags = JSON.parse(row.dataset.tags || "[]");
const matched = !tag || tags.includes(tag);
row.hidden = !matched;
if (matched) visible += 1;
}
if (status) {
const name = $("[data-tag-filter-name]", status);
const count = $("[data-tag-filter-count]", status);
if (name) name.textContent = tag ? `[${tag}]` : "";
if (count) count.textContent = `${visible} ${visible === 1 ? "post" : "posts"}`;
status.hidden = !tag;
status.classList.toggle("is-empty", visible === 0);
}
if (empty) empty.hidden = !tag || visible > 0;
}
function applyFromLocation() {
applyTag(new URLSearchParams(window.location.search).get("tag"));
}
for (const link of links) {
link.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
const nextTag = link.classList.contains("active") ? null : link.dataset.tag;
setUrl(nextTag);
applyTag(nextTag);
});
}
if (clear) {
clear.addEventListener("click", (event) => {
if (!isPlainLeftClick(event)) return;
event.preventDefault();
setUrl(null);
applyTag(null);
});
}
window.addEventListener("popstate", applyFromLocation);
window.addEventListener("filetabchange", applyFromLocation);
applyFromLocation();
}
function setupClickableRows() {
const rows = $$("[data-row-href]");
for (const row of rows) {
row.addEventListener("click", (event) => {
if (event.target.closest("a")) return;
const href = row.dataset.rowHref;
if (href) window.location.assign(href);
});
row.addEventListener("pointerdown", (event) => {
if (event.target.closest("a")) return;
row.classList.add("is-pressed");
});
for (const eventName of ["pointerup", "pointercancel", "pointerleave"]) {
row.addEventListener(eventName, () => row.classList.remove("is-pressed"));
}
}
}
function setupPrefetch() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection?.saveData || /(^|-)2g$/.test(connection?.effectiveType || "")) return;
const prefetched = new Set();
function prefetch(href) {
const url = new URL(href, window.location.href);
if (url.origin !== window.location.origin || url.hash || url.pathname === window.location.pathname) return;
if (prefetched.has(url.href)) return;
prefetched.add(url.href);
const hint = document.createElement("link");
hint.rel = "prefetch";
hint.as = "document";
hint.href = url.href;
document.head.append(hint);
}
for (const link of $$(".item-title[href], .post-title[href], .back-link[href], .post-button[href]")) {
link.addEventListener("pointerenter", () => prefetch(link.href), { once: true });
link.addEventListener("focus", () => prefetch(link.href), { once: true });
}
for (const row of $$("[data-row-href]")) {
row.addEventListener("pointerenter", () => prefetch(row.dataset.rowHref), { once: true });
}
}
setupFileTabs();
setupToc();
setupPostTags();
setupClickableRows();
setupPrefetch();
})();