/* ========================================================= STATE ========================================================= */ let index = []; let activeIndex = -1; const box = document.getElementById("search-box"); const results = document.getElementById("search-results"); /* ========================================================= LOAD SEARCH INDEX ========================================================= */ fetch("/search-index.json") .then(r => r.json()) .then(data => { index = Array.isArray(data) ? data : []; }) .catch(err => { console.error("Failed to load search index:", err); }); /* ========================================================= RENDER RESULTS ========================================================= */ function renderResults(items) { clearResults(); items.forEach((entry, i) => { const row = document.createElement("div"); row.dataset.index = i; const link = document.createElement("a"); link.href = entry.url; link.textContent = entry.title; link.addEventListener("click", ev => { ev.preventDefault(); ev.stopPropagation(); clearSearch(); pushPane(entry.url); }); row.appendChild(link); row.addEventListener("mouseenter", () => setActive(i)); row.addEventListener("mouseleave", clearActive); results.appendChild(row); }); } /* ========================================================= ACTIVE ITEM HANDLING ========================================================= */ function setActive(i) { const items = [...results.children]; items.forEach(el => el.classList.remove("active")); if (items[i]) { items[i].classList.add("active"); activeIndex = i; } } function clearActive() { [...results.children].forEach(el => el.classList.remove("active")); activeIndex = -1; } /* ========================================================= INPUT HANDLER ========================================================= */ box.addEventListener("input", () => { const query = box.value.trim().toLowerCase(); clearResults(); if (query.length < 2) return; const matches = index .map(entry => ({ ...entry, score: fuzzyScore(query, entry.title) })) .filter(entry => entry.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 20); // optional cap renderResults(matches); }); /* ========================================================= KEYBOARD NAVIGATION ========================================================= */ box.addEventListener("keydown", e => { const items = [...results.children]; if (!items.length) return; switch (e.key) { case "ArrowDown": e.preventDefault(); setActive((activeIndex + 1) % items.length); break; case "ArrowUp": e.preventDefault(); setActive((activeIndex - 1 + items.length) % items.length); break; case "Enter": if (activeIndex < 0) return; e.preventDefault(); e.stopPropagation(); const link = items[activeIndex].querySelector("a"); if (link) { clearSearch(); pushPane(link.getAttribute("href")); } break; case "Escape": clearSearch(); break; } }); /* ========================================================= CLICK OUTSIDE TO CLOSE ========================================================= */ document.addEventListener("click", e => { if (!e.target.closest(".banner-search")) { clearSearch(); } }); /* ========================================================= HELPERS ========================================================= */ function clearResults() { results.innerHTML = ""; } function clearSearch() { clearResults(); activeIndex = -1; } function fuzzyScore(query, text) { query = query.toLowerCase(); text = text.toLowerCase(); let score = 0; let qi = 0; let consecutive = 0; for (let ti = 0; ti < text.length && qi < query.length; ti++) { if (text[ti] === query[qi]) { qi++; consecutive++; score += 5 + consecutive * 2; // reward runs } else { consecutive = 0; } } if (qi !== query.length) return 0; score += Math.max(0, 20 - text.length); return score; }