2023-12-20 09:28:38 +00:00
|
|
|
const searchInputEl = document.getElementById("search-input");
|
2023-12-20 23:45:16 +00:00
|
|
|
|
2023-12-21 08:11:21 +00:00
|
|
|
// 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);
|
2023-12-20 09:28:38 +00:00
|
|
|
|
2023-12-21 08:11:21 +00:00
|
|
|
let lastValue = "";
|
2023-12-22 06:19:22 +00:00
|
|
|
let nextQueryId = 0;
|
|
|
|
let lastLoadedQueryId = -1;
|
2023-12-21 08:11:21 +00:00
|
|
|
async function updateSuggestions() {
|
|
|
|
const value = searchInputEl.value;
|
|
|
|
|
2023-12-22 06:19:22 +00:00
|
|
|
if (value === "") {
|
|
|
|
suggestionsEl.style.visibility = "hidden";
|
|
|
|
nextQueryId++;
|
|
|
|
lastLoadedQueryId = nextQueryId;
|
2023-12-21 08:11:21 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value === lastValue) {
|
|
|
|
suggestionsEl.style.visibility = "visible";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
lastValue = value;
|
2023-12-20 09:28:38 +00:00
|
|
|
|
2023-12-22 06:19:22 +00:00
|
|
|
const thisQueryId = nextQueryId;
|
|
|
|
nextQueryId++;
|
|
|
|
|
2023-12-20 23:17:46 +00:00
|
|
|
const res = await fetch(`/autocomplete?q=${encodeURIComponent(value)}`).then(
|
|
|
|
(res) => res.json()
|
|
|
|
);
|
2023-12-20 09:28:38 +00:00
|
|
|
const options = res[1];
|
|
|
|
|
2023-12-22 06:19:22 +00:00
|
|
|
// this makes sure we don't load suggestions out of order
|
|
|
|
if (thisQueryId < lastLoadedQueryId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
lastLoadedQueryId = thisQueryId;
|
|
|
|
|
2023-12-21 08:11:21 +00:00
|
|
|
renderSuggestions(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderSuggestions(options) {
|
|
|
|
if (options.length === 0) {
|
|
|
|
suggestionsEl.style.visibility = "hidden";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
suggestionsEl.style.visibility = "visible";
|
|
|
|
suggestionsEl.innerHTML = "";
|
2023-12-20 09:28:38 +00:00
|
|
|
options.forEach((option) => {
|
2023-12-21 08:11:21 +00:00
|
|
|
const optionEl = document.createElement("div");
|
|
|
|
optionEl.textContent = option;
|
|
|
|
optionEl.className = "search-input-suggestion";
|
|
|
|
suggestionsEl.appendChild(optionEl);
|
2023-12-22 00:43:29 +00:00
|
|
|
|
|
|
|
optionEl.addEventListener("mousedown", () => {
|
2023-12-21 08:11:21 +00:00
|
|
|
searchInputEl.value = option;
|
|
|
|
searchInputEl.focus();
|
|
|
|
searchInputEl.form.submit();
|
|
|
|
});
|
2023-12-20 09:28:38 +00:00
|
|
|
});
|
2023-12-21 08:11:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-12-21 05:17:39 +00:00
|
|
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
2023-12-21 08:11:21 +00:00
|
|
|
// 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;
|
|
|
|
}
|
2023-12-21 05:17:39 +00:00
|
|
|
// must be a letter or number
|
2023-12-21 08:11:21 +00:00
|
|
|
if (e.key.match(/^[a-z0-9]$/i)) {
|
2023-12-21 05:17:39 +00:00
|
|
|
searchInputEl.focus();
|
|
|
|
}
|
2023-12-21 08:11:21 +00:00
|
|
|
// 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);
|
|
|
|
}
|
2023-12-21 09:00:18 +00:00
|
|
|
// backspace key focuses it at the end
|
|
|
|
else if (e.key === "Backspace") {
|
|
|
|
searchInputEl.focus();
|
|
|
|
searchInputEl.setSelectionRange(
|
|
|
|
searchInputEl.value.length,
|
|
|
|
searchInputEl.value.length
|
|
|
|
);
|
|
|
|
}
|
2023-12-21 08:11:21 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// update the input suggestions on input
|
|
|
|
searchInputEl.addEventListener("input", () => {
|
|
|
|
clearFocusedSuggestion();
|
|
|
|
updateSuggestions();
|
2023-12-21 05:17:39 +00:00
|
|
|
});
|
2023-12-23 08:10:17 +00:00
|
|
|
// and when they click suggestions
|
2023-12-22 06:19:22 +00:00
|
|
|
searchInputEl.addEventListener("click", updateSuggestions);
|
2023-12-21 09:00:18 +00:00
|
|
|
// on unfocus hide the suggestions
|
2023-12-22 00:43:29 +00:00
|
|
|
searchInputEl.addEventListener("blur", (e) => {
|
2023-12-21 09:00:18 +00:00
|
|
|
suggestionsEl.style.visibility = "hidden";
|
|
|
|
});
|