diff --git a/src/web/assets/index.html b/src/web/assets/index.html
index 335edca..2557b10 100644
--- a/src/web/assets/index.html
+++ b/src/web/assets/index.html
@@ -12,7 +12,7 @@
metasearch
-
diff --git a/src/web/assets/script.js b/src/web/assets/script.js
index ec87e9f..587c81c 100644
--- a/src/web/assets/script.js
+++ b/src/web/assets/script.js
@@ -1,37 +1,129 @@
const searchInputEl = document.getElementById("search-input");
-// add a datalist after the search input
-const datalistEl = document.createElement("datalist");
-datalistEl.id = "search-input-datalist";
-searchInputEl.setAttribute("list", datalistEl.id);
-searchInputEl.insertAdjacentElement("afterend", datalistEl);
+// add an element with search suggestions after the search input
+const suggestionsEl = document.createElement("div");
+suggestionsEl.id = "search-input-suggestions";
+suggestionsEl.style.visibility = "hidden";
+searchInputEl.insertAdjacentElement("afterend", suggestionsEl);
-// update the datalist options on input
-searchInputEl.addEventListener("input", async (e) => {
- const value = e.target.value;
+let lastValue = "";
+async function updateSuggestions() {
+ const value = searchInputEl.value;
+
+ if (value.trim() === "") {
+ renderSuggestions([]);
+ return;
+ }
+
+ if (value === lastValue) {
+ suggestionsEl.style.visibility = "visible";
+ return;
+ }
+ lastValue = value;
const res = await fetch(`/autocomplete?q=${encodeURIComponent(value)}`).then(
(res) => res.json()
);
const options = res[1];
- datalistEl.innerHTML = "";
- options.forEach((option) => {
- const optionEl = document.createElement("option");
- optionEl.value = option;
- datalistEl.appendChild(optionEl);
- });
-});
+ renderSuggestions(options);
+}
+
+function renderSuggestions(options) {
+ if (options.length === 0) {
+ suggestionsEl.style.visibility = "hidden";
+ return;
+ }
+
+ suggestionsEl.style.visibility = "visible";
+ suggestionsEl.innerHTML = "";
+ options.forEach((option) => {
+ const optionEl = document.createElement("div");
+ optionEl.textContent = option;
+ optionEl.className = "search-input-suggestion";
+ suggestionsEl.appendChild(optionEl);
+ optionEl.addEventListener("click", () => {
+ searchInputEl.value = option;
+ searchInputEl.focus();
+ searchInputEl.form.submit();
+ });
+ });
+}
+
+let focusedSuggestionIndex = -1;
+let focusedSuggestionEl = null;
+
+function clearFocusedSuggestion() {
+ if (focusedSuggestionEl) {
+ focusedSuggestionEl.classList.remove("focused");
+ focusedSuggestionEl = null;
+ focusedSuggestionIndex = -1;
+ }
+}
+
+function focusSelectionIndex(index) {
+ clearFocusedSuggestion();
+ focusedSuggestionIndex = index;
+ focusedSuggestionEl = suggestionsEl.children[focusedSuggestionIndex];
+ focusedSuggestionEl.classList.add("focused");
+ searchInputEl.value = focusedSuggestionEl.textContent;
+}
-// if the user starts typing but they don't have focus on the input, focus it
document.addEventListener("keydown", (e) => {
+ // if it's focused then use different keybinds
+ if (searchInputEl.matches(":focus")) {
+ if (e.key === "ArrowDown") {
+ e.preventDefault();
+ if (focusedSuggestionIndex === -1) {
+ focusSelectionIndex(0);
+ } else if (focusedSuggestionIndex < suggestionsEl.children.length - 1) {
+ focusSelectionIndex(focusedSuggestionIndex + 1);
+ } else {
+ focusSelectionIndex(0);
+ }
+ } else if (e.key === "ArrowUp") {
+ e.preventDefault();
+ if (focusedSuggestionIndex === -1) {
+ focusSelectionIndex(suggestionsEl.children.length - 1);
+ } else if (focusedSuggestionIndex > 0) {
+ focusSelectionIndex(focusedSuggestionIndex - 1);
+ } else {
+ focusSelectionIndex(suggestionsEl.children.length - 1);
+ }
+ }
+
+ return;
+ }
+
+ // if the user starts typing but they don't have focus on the input, focus it
+
+ // no modifier keys
+ if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
+ return;
+ }
// must be a letter or number
- if (
- e.key.match(/^[a-z0-9]$/i) &&
- !searchInputEl.matches(":focus") &&
- !e.ctrlKey &&
- !e.metaKey
- ) {
+ if (e.key.match(/^[a-z0-9]$/i)) {
searchInputEl.focus();
}
+ // right arrow key focuses it at the end
+ else if (e.key === "ArrowRight") {
+ searchInputEl.focus();
+ searchInputEl.setSelectionRange(
+ searchInputEl.value.length,
+ searchInputEl.value.length
+ );
+ }
+ // left arrow key focuses it at the beginning
+ else if (e.key === "ArrowLeft") {
+ searchInputEl.focus();
+ searchInputEl.setSelectionRange(0, 0);
+ }
});
+
+// update the input suggestions on input
+searchInputEl.addEventListener("input", () => {
+ clearFocusedSuggestion();
+ updateSuggestions();
+});
+// and on focus
+searchInputEl.addEventListener("focus", updateSuggestions);
diff --git a/src/web/assets/style.css b/src/web/assets/style.css
index 8d2fbde..bc5f754 100644
--- a/src/web/assets/style.css
+++ b/src/web/assets/style.css
@@ -21,7 +21,7 @@ main {
background-color: #0d1017;
min-height: 100%;
}
-@media screen and (max-width: 80rem) {
+@media screen and (max-width: 74rem) {
/* small screens */
.results-container {
margin: 0 auto;
@@ -36,11 +36,11 @@ input {
font-size: inherit;
padding: 0.25rem;
}
-input:focus {
+input:focus-visible {
outline: none;
border-color: #e6b450;
}
-:focus {
+:focus-visible {
outline: 1px solid #e6b450;
}
input[type="submit"] {
@@ -62,8 +62,9 @@ a:visited {
height: 100%;
justify-content: center;
margin: 0 auto;
- width: fit-content;
+ padding: 0 0.5em;
text-align: center;
+ max-width: 30em;
}
h1 {
margin-top: 0;
@@ -79,6 +80,22 @@ h1 {
max-width: 30em;
flex: 1;
}
+#search-input-suggestions {
+ position: absolute;
+ text-align: left;
+ margin-top: calc(1.9em + 1px);
+ background: #0f131a;
+ padding: 0.1em 0 0.3em 0;
+ border: 1px solid #234;
+ border-top: transparent;
+}
+.search-input-suggestion {
+ cursor: pointer;
+ padding: 0 0.3em;
+}
+.search-input-suggestion.focused {
+ background: #234;
+}
/* search result */
.search-result {
@@ -181,7 +198,7 @@ h1 {
max-width: 30rem;
margin-left: 42rem;
}
-@media screen and (max-width: 80rem) {
+@media screen and (max-width: 74rem) {
/* small screens */
.infobox {
position: static;