diff --git a/src/client/search-init.ts b/src/client/search-init.ts index 3b58edd06..5e2d80c13 100644 --- a/src/client/search-init.ts +++ b/src/client/search-init.ts @@ -22,7 +22,6 @@ addEventListener("keydown", (event) => { // persistently after you blur the search input.) toggle.classList.add("observablehq-sidebar-open"); input.focus(); - input.select(); event.preventDefault(); } }); diff --git a/src/client/search.js b/src/client/search.js index d86347fb1..10b4f677d 100644 --- a/src/client/search.js +++ b/src/client/search.js @@ -40,15 +40,33 @@ input.addEventListener("input", () => { resultsContainer.innerHTML = results.length === 0 ? "
no results
" - : `
${results.length.toLocaleString("en-US")} result${results.length === 1 ? "" : "s"}
    ${results - .map(renderResult) - .join("")}
`; + : `
${results.length.toLocaleString("en-US")} result${ + results.length === 1 ? "" : "s" + }
    ${renderResults(results)}
`; + resultsContainer.querySelector(`.${activeClass}`)?.scrollIntoView({block: "nearest"}); + for (const a of resultsContainer.querySelectorAll("a")) a.onclick = captureQuery; }); -function renderResult({id, score, title}, i) { - return `
  • ${escapeText(String(title ?? "—"))}
  • `; +function renderResults(results) { + const me = document.location.href.replace(/[?#].*/, ""); + let found; + results = results.map(({id, score, title}) => { + const href = import.meta.resolve(`../${id}`); + return { + title: String(title ?? "—"), + href, + score: Math.min(5, Math.round(0.6 * score)), + active: me === href && (found = true) + }; + }); + if (!found) results[0].active = true; + return results.map(renderResult).join(""); +} + +function renderResult({href, score, title, active}) { + return `
  • ${escapeText(title)}
  • `; } function escapeDoubleQuote(text) { @@ -66,18 +84,57 @@ function entity(character) { // Handle a race condition where an input event fires while awaiting the index fetch. input.dispatchEvent(new Event("input")); +// Capture 10 previous queries and restore them on ArrowUp/Down. +function captureQuery() { + sessionStorage.setItem( + "search-queries", + JSON.stringify((queries = Array.from(new Set([...(input.value && [input.value]), ...queries])).slice(0, 10))) + ); +} +let queries = []; +try { + queries = JSON.parse(sessionStorage.getItem("search-queries") ?? "[]"); + if (!Array.isArray(queries)) queries = []; +} catch (error) { + // ignore parse errors +} +input.addEventListener("blur", captureQuery); +input.addEventListener("cancel", captureQuery); + input.addEventListener("keydown", (event) => { const {code} = event; if (code === "Escape" && input.value === "") return input.blur(); if (code === "ArrowDown" || code === "ArrowUp" || code === "Enter") { - const results = resultsContainer.querySelector("ol"); - if (!results) return; - let activeResult = results.querySelector(`.${activeClass}`); - if (code === "Enter") return activeResult.querySelector("a").click(); - activeResult.classList.remove(activeClass); - if (code === "ArrowUp") activeResult = activeResult.previousElementSibling ?? results.lastElementChild; - else activeResult = activeResult.nextElementSibling ?? results.firstElementChild; - activeResult.classList.add(activeClass); - activeResult.scrollIntoView({block: "nearest"}); + if (input.selectionStart == 0 && input.selectionEnd == input.value.length) { + if (code === "Enter") { + input.selectionStart = input.selectionEnd = input.value.length; + } else { + const i = queries.indexOf(input.value); + const query = + code === "ArrowUp" && queries.length > i + 1 + ? queries[i + 1] + : code === "ArrowDown" && i > 0 + ? queries[i - 1] + : null; + if (query) { + if (i === -1) captureQuery(); // capture current query if necessary. + input.value = query; + input.select(); + input.dispatchEvent(new Event("input")); + } + } + event.preventDefault(); + } else { + const results = resultsContainer.querySelector("ol"); + if (results) { + let activeResult = results.querySelector(`.${activeClass}`); + if (code === "Enter") return activeResult.querySelector("a").click(); + activeResult.classList.remove(activeClass); + if (code === "ArrowUp") activeResult = activeResult.previousElementSibling ?? results.lastElementChild; + else activeResult = activeResult.nextElementSibling ?? results.firstElementChild; + activeResult.classList.add(activeClass); + activeResult.scrollIntoView({block: "nearest"}); + } + } } });