better search suggestions
This commit is contained in:
parent
fbd12e8556
commit
e6239134a4
@ -12,7 +12,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h1>metasearch</h1>
|
<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="text" name="q" placeholder="Search" id="search-input" autofocus onfocus="this.select()" autocomplete="off">
|
||||||
<input type="submit" value="Search">
|
<input type="submit" value="Search">
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,37 +1,129 @@
|
|||||||
const searchInputEl = document.getElementById("search-input");
|
const searchInputEl = document.getElementById("search-input");
|
||||||
|
|
||||||
// add a datalist after the search input
|
// add an element with search suggestions after the search input
|
||||||
const datalistEl = document.createElement("datalist");
|
const suggestionsEl = document.createElement("div");
|
||||||
datalistEl.id = "search-input-datalist";
|
suggestionsEl.id = "search-input-suggestions";
|
||||||
searchInputEl.setAttribute("list", datalistEl.id);
|
suggestionsEl.style.visibility = "hidden";
|
||||||
searchInputEl.insertAdjacentElement("afterend", datalistEl);
|
searchInputEl.insertAdjacentElement("afterend", suggestionsEl);
|
||||||
|
|
||||||
// update the datalist options on input
|
let lastValue = "";
|
||||||
searchInputEl.addEventListener("input", async (e) => {
|
async function updateSuggestions() {
|
||||||
const value = e.target.value;
|
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(
|
const res = await fetch(`/autocomplete?q=${encodeURIComponent(value)}`).then(
|
||||||
(res) => res.json()
|
(res) => res.json()
|
||||||
);
|
);
|
||||||
const options = res[1];
|
const options = res[1];
|
||||||
|
|
||||||
datalistEl.innerHTML = "";
|
renderSuggestions(options);
|
||||||
options.forEach((option) => {
|
}
|
||||||
const optionEl = document.createElement("option");
|
|
||||||
optionEl.value = option;
|
function renderSuggestions(options) {
|
||||||
datalistEl.appendChild(optionEl);
|
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) => {
|
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
|
// must be a letter or number
|
||||||
if (
|
if (e.key.match(/^[a-z0-9]$/i)) {
|
||||||
e.key.match(/^[a-z0-9]$/i) &&
|
|
||||||
!searchInputEl.matches(":focus") &&
|
|
||||||
!e.ctrlKey &&
|
|
||||||
!e.metaKey
|
|
||||||
) {
|
|
||||||
searchInputEl.focus();
|
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);
|
||||||
|
@ -21,7 +21,7 @@ main {
|
|||||||
background-color: #0d1017;
|
background-color: #0d1017;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 80rem) {
|
@media screen and (max-width: 74rem) {
|
||||||
/* small screens */
|
/* small screens */
|
||||||
.results-container {
|
.results-container {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -36,11 +36,11 @@ input {
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
input:focus {
|
input:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #e6b450;
|
border-color: #e6b450;
|
||||||
}
|
}
|
||||||
:focus {
|
:focus-visible {
|
||||||
outline: 1px solid #e6b450;
|
outline: 1px solid #e6b450;
|
||||||
}
|
}
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
@ -62,8 +62,9 @@ a:visited {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: fit-content;
|
padding: 0 0.5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@ -79,6 +80,22 @@ h1 {
|
|||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
flex: 1;
|
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 */
|
||||||
.search-result {
|
.search-result {
|
||||||
@ -181,7 +198,7 @@ h1 {
|
|||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
margin-left: 42rem;
|
margin-left: 42rem;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 80rem) {
|
@media screen and (max-width: 74rem) {
|
||||||
/* small screens */
|
/* small screens */
|
||||||
.infobox {
|
.infobox {
|
||||||
position: static;
|
position: static;
|
||||||
|
Loading…
Reference in New Issue
Block a user