// Sidebar toggle, "on this page" highlighting, and client-side search. const toggle = document.querySelector(".nav-toggle"); const sidebar = document.getElementById("sidebar"); if (toggle && sidebar) { const setOpen = (open) => { sidebar.classList.toggle("is-open", open); toggle.setAttribute("aria-expanded", String(open)); }; toggle.addEventListener("click", () => setOpen(!sidebar.classList.contains("is-open"))); document.addEventListener("keydown", (e) => { if (e.key === "Escape") setOpen(false); }); document.addEventListener("click", (e) => { if (sidebar.classList.contains("is-open") && !sidebar.contains(e.target) && !toggle.contains(e.target)) setOpen(false); }); const current = sidebar.querySelector('[aria-current="page"]'); if (current) current.scrollIntoView({ block: "nearest" }); } // Highlight the current heading in the table of contents. const tocLinks = [...document.querySelectorAll(".toc a")]; if (tocLinks.length && "IntersectionObserver" in window) { const map = new Map(tocLinks.map((a) => [decodeURIComponent(a.hash.slice(1)), a])); const headings = [...map.keys()].map((id) => document.getElementById(id)).filter(Boolean); const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { tocLinks.forEach((a) => a.classList.remove("is-active")); map.get(entry.target.id)?.classList.add("is-active"); } }); }, { rootMargin: "0px 0px -70% 0px" }); headings.forEach((h) => observer.observe(h)); } // Search const input = document.getElementById("search-input"); const results = document.getElementById("search-results"); const script = document.currentScript || document.querySelector("script[data-index]"); const indexURL = script ? script.dataset.index : "/index.json"; let index = null; let selected = -1; async function loadIndex() { if (index) return index; try { const res = await fetch(indexURL); index = await res.json(); } catch (err) { index = []; } return index; } const norm = (s) => (s || "").toLowerCase(); function score(item, terms) { let total = 0; for (const t of terms) { const title = norm(item.title); let s = 0; if (title === t) s += 100; else if (title.startsWith(t)) s += 60; else if (title.includes(t)) s += 40; if (norm(item.replaces).includes(t)) s += 30; if (norm(item.tagline).includes(t)) s += 15; if (norm(item.headings).includes(t)) s += 12; if (norm(item.body).includes(t)) s += 4; if (!s) return 0; // every term must match somewhere total += s; } return total; } function escapeHTML(s) { return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } function render(items, query) { selected = -1; if (!query) { results.hidden = true; input.setAttribute("aria-expanded", "false"); return; } results.hidden = false; input.setAttribute("aria-expanded", "true"); if (!items.length) { results.innerHTML = `
  • No pages match “${escapeHTML(query)}”. Try an app name or a service you want to replace, like “Dropbox”.
  • `; return; } results.innerHTML = items.slice(0, 8).map((it) => { const meta = it.replaces ? `${it.section}. Replaces ${it.replaces}` : it.section; return `
  • ${escapeHTML(it.title)}${escapeHTML(meta)}
  • `; }).join(""); } async function search() { const q = input.value.trim(); const data = await loadIndex(); const terms = norm(q).split(/\s+/).filter(Boolean); const hits = data .map((item) => ({ item, s: score(item, terms) })) .filter((x) => x.s > 0) .sort((a, b) => b.s - a.s) .map((x) => x.item); render(hits, q); } function move(delta) { const links = [...results.querySelectorAll("a")]; if (!links.length) return; selected = (selected + delta + links.length) % links.length; links.forEach((a, i) => a.setAttribute("aria-selected", String(i === selected))); links[selected].scrollIntoView({ block: "nearest" }); } if (input && results) { input.addEventListener("focus", loadIndex, { once: true }); input.addEventListener("input", search); input.addEventListener("keydown", (e) => { if (e.key === "ArrowDown") { e.preventDefault(); move(1); } else if (e.key === "ArrowUp") { e.preventDefault(); move(-1); } else if (e.key === "Enter") { const links = results.querySelectorAll("a"); const target = links[selected >= 0 ? selected : 0]; if (target) { e.preventDefault(); window.location.href = target.href; } } else if (e.key === "Escape") { input.value = ""; render([], ""); input.blur(); } }); document.addEventListener("keydown", (e) => { if (e.key === "/" && !/input|textarea|select/i.test(document.activeElement.tagName)) { e.preventDefault(); input.focus(); } }); document.addEventListener("click", (e) => { if (!e.target.closest(".search")) { results.hidden = true; input.setAttribute("aria-expanded", "false"); } }); }