diff --git a/assets/scripts/search.js b/assets/scripts/search.js index cad145d..f45c851 100755 --- a/assets/scripts/search.js +++ b/assets/scripts/search.js @@ -1,27 +1,47 @@ let lunrIndex; let documents = []; let activeIndex = -1; +let currentResults = []; +let isSyncingHeaderInput = false; +let suppressHeaderFocus = false; +let searchReady = false; + const searchInput = document.getElementById("search-input"); const searchBtn = document.getElementById("search-btn"); -const resultsBox = document.createElement("div"); -const searchForm = searchInput.closest("form"); +const searchForm = searchInput?.closest("form"); + const modal = document.createElement("div"); -const modalResults = modal.querySelector("#search-modal-results"); - -resultsBox.id = "search-results"; -document.body.appendChild(resultsBox); - modal.id = "search-modal"; +modal.setAttribute("aria-hidden", "true"); modal.innerHTML = ` -
- + + `; - document.body.appendChild(modal); +const pane = modal.querySelector(".search-modal-content"); +const paneInput = modal.querySelector("#search-pane-input"); +const modalResults = modal.querySelector("#search-modal-results"); +const paneCount = modal.querySelector("#search-pane-count"); + function extractDocuments(node, currentPath = "") { if (node.type === "folder" && node.children) { const nextPath = currentPath ? `${currentPath}/${node.name}` : node.name; @@ -53,11 +73,18 @@ function buildIndex() { } async function initSearch() { - const response = await fetch("/test.json"); - const json = await response.json(); + try { + const response = await fetch("/test.json"); + const json = await response.json(); - extractDocuments(json); - buildIndex(); + extractDocuments(json); + buildIndex(); + searchReady = true; + if (isSearchOpen()) renderResults(paneInput.value); + } catch (error) { + console.warn("Search index could not be loaded", error); + if (paneCount) paneCount.textContent = "Search is unavailable right now."; + } } initSearch(); @@ -71,7 +98,7 @@ function buildExactQuery(query) { .join(" "); } -function buildSnippet(content, terms, radius = 80) { +function buildSnippet(content, terms, radius = 90) { if (!content || terms.length === 0) return ""; const lower = content.toLowerCase(); @@ -107,14 +134,23 @@ function getSearchTerms(query) { .filter((t) => t.length > 2); } +function escapeHtml(text) { + return String(text || "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + function highlight(text, terms) { - if (!text || terms.length === 0) return text; + const escapedText = escapeHtml(text); + if (!escapedText || terms.length === 0) return escapedText; - const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + const escapedTerms = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + const regex = new RegExp(`(${escapedTerms.join("|")})`, "gi"); - const regex = new RegExp(`(${escaped.join("|")})`, "gi"); - - return text.replace(regex, "$1"); + return escapedText.replace(regex, "$1"); } function search(query) { @@ -143,187 +179,272 @@ function search(query) { .filter(Boolean); } -function showResults(results) { - resultsBox.innerHTML = ""; - activeIndex = -1; +function setHeaderSearchValue(value, dispatchInput = false) { + if (!searchInput) return; - if (results.length === 0) { - resultsBox.style.display = "none"; - return; + isSyncingHeaderInput = true; + searchInput.value = value; + isSyncingHeaderInput = false; + + if (dispatchInput) { + searchInput.dispatchEvent(new Event("input", { bubbles: true })); } +} - const rect = searchInput.getBoundingClientRect(); - resultsBox.style.top = `${rect.bottom + window.scrollY}px`; - resultsBox.style.left = `${rect.left + window.scrollX}px`; - resultsBox.style.width = `${rect.width}px`; +function isSearchOpen() { + return modal.classList.contains("is-open"); +} - results.forEach((result, i) => { - const item = document.createElement("div"); - item.className = "search-result"; - item.dataset.index = i; +function openSearchPane(initialValue = "") { + const value = initialValue || searchInput?.value || paneInput?.value || ""; - item.innerHTML = ` -No results found.
"; + if (!trimmed) { + paneCount.textContent = "Start typing to search your notes."; + modalResults.innerHTML = ` +