better search suggestions

This commit is contained in:
mat 2023-12-21 02:11:21 -06:00
parent fbd12e8556
commit e6239134a4
3 changed files with 137 additions and 28 deletions

View File

@ -12,7 +12,7 @@
<body>
<div class="main-container">
<h1>metasearch</h1>
<form action="/search" method="get">
<form action="/search" method="get" class="search-form">
<input type="text" name="q" placeholder="Search" id="search-input" autofocus onfocus="this.select()" autocomplete="off">
<input type="submit" value="Search">
</form>

View File

@ -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 = "";
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("option");
optionEl.value = option;
datalistEl.appendChild(optionEl);
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;
}
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
document.addEventListener("keydown", (e) => {
// 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);

View File

@ -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;