From 1a91d4c0880b97bd6a6734f333fb5cf954afa835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 22 Feb 2024 22:14:33 +0100 Subject: [PATCH 1/5] save search query, and restore on cmd-K closes #889 --- src/client/search-init.ts | 1 + src/client/search.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/client/search-init.ts b/src/client/search-init.ts index 3b58edd06..b774b2bcc 100644 --- a/src/client/search-init.ts +++ b/src/client/search-init.ts @@ -22,6 +22,7 @@ addEventListener("keydown", (event) => { // persistently after you blur the search input.) toggle.classList.add("observablehq-sidebar-open"); input.focus(); + input.value = sessionStorage.getItem("search-query") ?? ""; input.select(); event.preventDefault(); } diff --git a/src/client/search.js b/src/client/search.js index d86347fb1..7dc3af49e 100644 --- a/src/client/search.js +++ b/src/client/search.js @@ -28,6 +28,7 @@ const index = await fetch(import.meta.resolve(global.__minisearch)) input.addEventListener("input", () => { if (currentValue === input.value) return; currentValue = input.value; + sessionStorage.setItem("search-query", currentValue); if (!currentValue.length) { container.setAttribute("data-shortcut", shortcut); sidebar.classList.remove("observablehq-search-results"); From 1ec16cda49efa19d571ab5b659609485a490c3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 23 Feb 2024 14:46:51 +0100 Subject: [PATCH 2/5] highlight the current page if present in the search results --- src/client/search.js | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/client/search.js b/src/client/search.js index 7dc3af49e..1349780ec 100644 --- a/src/client/search.js +++ b/src/client/search.js @@ -41,15 +41,32 @@ 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"}); }); -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) { From 03adb161f5e37714d9a3b82b2fece4129fe58fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 27 Feb 2024 22:30:24 +0100 Subject: [PATCH 3/5] ignore url hash (e.g. maplibre-gl#12/47.27574/11.39085/0/52) --- src/client/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/search.js b/src/client/search.js index 1349780ec..60bd9111e 100644 --- a/src/client/search.js +++ b/src/client/search.js @@ -48,7 +48,7 @@ input.addEventListener("input", () => { }); function renderResults(results) { - const me = document.location.href.replace(/[?].*/, ""); + const me = document.location.href.replace(/[?#].*/, ""); let found; results = results.map(({id, score, title}) => { const href = import.meta.resolve(`../${id}`); From 7ab9c171820c81137bfb57e5384e90518aa2435b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 12 Mar 2024 11:36:56 +0100 Subject: [PATCH 4/5] restore search query on focus --- src/client/search-init.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/search-init.ts b/src/client/search-init.ts index b774b2bcc..90ce88bdd 100644 --- a/src/client/search-init.ts +++ b/src/client/search-init.ts @@ -8,6 +8,10 @@ const input = container.querySelector("input")!; const load = () => import("observablehq:search"); input.addEventListener("focus", load, {once: true}); input.addEventListener("keydown", load, {once: true}); +input.addEventListener("focus", () => { + input.value = sessionStorage.getItem("search-query") ?? ""; + input.select(); +}); // Focus on meta-K and / const toggle = document.querySelector("#observablehq-sidebar-toggle")!; @@ -22,8 +26,6 @@ addEventListener("keydown", (event) => { // persistently after you blur the search input.) toggle.classList.add("observablehq-sidebar-open"); input.focus(); - input.value = sessionStorage.getItem("search-query") ?? ""; - input.select(); event.preventDefault(); } }); From 372b942ec9e1bc25d044bb05f07a3e69d578e353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 12 Mar 2024 13:37:05 +0100 Subject: [PATCH 5/5] capture 10 recent queries and navigate between them when the search input is fully selected. --- src/client/search-init.ts | 4 --- src/client/search.js | 61 ++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/client/search-init.ts b/src/client/search-init.ts index 90ce88bdd..5e2d80c13 100644 --- a/src/client/search-init.ts +++ b/src/client/search-init.ts @@ -8,10 +8,6 @@ const input = container.querySelector("input")!; const load = () => import("observablehq:search"); input.addEventListener("focus", load, {once: true}); input.addEventListener("keydown", load, {once: true}); -input.addEventListener("focus", () => { - input.value = sessionStorage.getItem("search-query") ?? ""; - input.select(); -}); // Focus on meta-K and / const toggle = document.querySelector("#observablehq-sidebar-toggle")!; diff --git a/src/client/search.js b/src/client/search.js index 60bd9111e..10b4f677d 100644 --- a/src/client/search.js +++ b/src/client/search.js @@ -28,7 +28,6 @@ const index = await fetch(import.meta.resolve(global.__minisearch)) input.addEventListener("input", () => { if (currentValue === input.value) return; currentValue = input.value; - sessionStorage.setItem("search-query", currentValue); if (!currentValue.length) { container.setAttribute("data-shortcut", shortcut); sidebar.classList.remove("observablehq-search-results"); @@ -44,7 +43,8 @@ input.addEventListener("input", () => { : `
    ${results.length.toLocaleString("en-US")} result${ results.length === 1 ? "" : "s" }
      ${renderResults(results)}
    `; - resultsContainer.querySelector(`.${activeClass}`).scrollIntoView({block: "nearest"}); + resultsContainer.querySelector(`.${activeClass}`)?.scrollIntoView({block: "nearest"}); + for (const a of resultsContainer.querySelectorAll("a")) a.onclick = captureQuery; }); function renderResults(results) { @@ -84,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"}); + } + } } });