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"});
+ }
+ }
}
});