(search) Fix typeahead suggestions, as well as improve mobile and desktop UX in small ways.

This commit is contained in:
Viktor Lofgren 2023-12-01 16:36:45 +01:00
parent d530c3096f
commit 96357e9bfd
5 changed files with 163 additions and 88 deletions

View File

@ -1,23 +1,47 @@
// This sets the data-has-js attribute on the body tag to true, so we can style the page with the assumption that
// the browser supports JS. This is a progressive enhancement, so the page will still work without JS.
document.getElementsByTagName('body')[0].setAttribute('data-has-js', 'true'); document.getElementsByTagName('body')[0].setAttribute('data-has-js', 'true');
const registerCloseButton = () => {
// Add a button to close the filters for mobile; we do this in js to not pollute the DOM for text-only browsers
const closeButton = document.createElement('button');
closeButton.setAttribute('id', 'menu-close');
closeButton.setAttribute('title', 'Close the menu');
closeButton.setAttribute('aria-controls', '#filters');
closeButton.innerHTML = 'X';
closeButton.onclick = (event) => {
document.getElementById('filters').style.display = 'none';
event.stopPropagation();
return false;
}
document.getElementById('filters').getElementsByTagName('h2')[0].append(closeButton);
}
// Add a button to open the filters for mobile; we do this in js to not pollute the DOM for text-only browsers // Add a button to open the filters for mobile; we do this in js to not pollute the DOM for text-only browsers
const button = document.createElement('button'); const filtersButton = document.createElement('button');
button.setAttribute('id', 'mcfeast'); filtersButton.setAttribute('id', 'mcfeast');
button.setAttribute('aria-controls', '#filters'); filtersButton.setAttribute('aria-controls', '#filters');
button.onclick = (event) => { filtersButton.onclick = (event) => {
setDisplay(document.getElementById('filters'), 'block'); // Defer creation of the close button until the menu is opened. This is needed because the script for creating
// the filter button is run early to avoid layout shifts.
if (document.getElementById('menu-close') === null) {
registerCloseButton();
}
document.getElementById('filters').style.display = 'block';
event.stopPropagation(); event.stopPropagation();
return false; return false;
} }
button.innerHTML = 'Filters'; filtersButton.innerHTML = 'Filters';
document.getElementById('search-box').getElementsByTagName('h1')[0].append(button); document.getElementById('search-box').getElementsByTagName('h1')[0].append(filtersButton);
function setDisplay(element, value) { // To prevent the filter menu from being opened when the user hits enter on the search box, we need to add a keydown
element.style.display = value; // handler to the search box that stops the event from propagating. Janky hack, but it works.
} document.getElementById('query').addEventListener('keydown', e=> {
if (e.key === "Enter") {
document.getElementById('menu-close').onclick = (event) => { const form = document.getElementById('search-form');
setDisplay(document.getElementById('filters'), 'none'); form.submit();
event.stopPropagation(); e.preventDefault();
return false;
} }
});

View File

@ -272,6 +272,7 @@ section.cards {
margin-left: 1ch; margin-left: 1ch;
} }
footer { footer {
clear: both; clear: both;
@ -348,7 +349,7 @@ footer {
padding: 0.5ch; padding: 0.5ch;
background-color: $fg-light; background-color: $fg-light;
display: grid; display: grid;
grid-template-columns: max-content auto max-content; grid-template-columns: max-content 0 auto max-content;
grid-gap: 0.5ch; grid-gap: 0.5ch;
grid-auto-rows: minmax(1ch, auto); grid-auto-rows: minmax(1ch, auto);
width: 100%; width: 100%;
@ -366,6 +367,11 @@ footer {
text-align: center; text-align: center;
} }
#suggestions-anchor {
margin: -0.5ch; // We need this anchor for the typeahead suggestions, but we don't want it to affect the layout
padding: 0;
}
input[type="text"] { input[type="text"] {
font-family: monospace; font-family: monospace;
font-size: 12pt; font-size: 12pt;
@ -375,13 +381,42 @@ footer {
color: $fg-dark; color: $fg-dark;
} }
button[type="submit"] { input[type="submit"] {
font-size: 12pt; font-size: 12pt;
border: 1px solid $border-color; border: 1px solid $border-color;
background-color: $fg-light; background-color: $fg-light;
color: $fg-dark; color: $fg-dark;
} }
.suggestions {
background-color: #fff;
padding: .5ch;
margin-top: 5.5ch;
margin-left: 1ch;
position: absolute;
display: inline-block;
width: 300px;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
box-shadow: 5px 5px 5px #888;
z-index: 10;
a {
display: block;
color: #000;
font-size: 12pt;
font-family: 'fixedsys', monospace, serif;
text-decoration: none;
outline: none;
}
a:focus {
display: block;
background-color: #000;
color: #eee;
}
}
} }
.filter-toggle-on { .filter-toggle-on {
@ -519,6 +554,8 @@ footer {
padding: 0 0 0 0 !important; padding: 0 0 0 0 !important;
max-width: 100%; max-width: 100%;
#suggestions-anchor { display: none; } // suggestions are not useful on mobile
.sidebar-narrow { .sidebar-narrow {
display: block; // fix for bizarre chrome rendering issue display: block; // fix for bizarre chrome rendering issue
} }

View File

@ -1,24 +1,29 @@
if(!window.matchMedia("(pointer: coarse)").matches) { function setupTypeahead() {
query = document.getElementById('query'); const query = document.getElementById('query');
query.setAttribute('autocomplete', 'off'); query.setAttribute('autocomplete', 'off');
timer = null; const queryBox = document.getElementById('suggestions-anchor');
let timer = null;
function fetchSuggestions(e) { function fetchSuggestions(e) {
if (timer != null) { if (timer != null) {
clearTimeout(timer); clearTimeout(timer);
} }
timer = setTimeout(() => { timer = setTimeout(() => {
req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.onload = rsp => { req.onload = rsp => {
items = JSON.parse(req.responseText); let items = JSON.parse(req.responseText);
var old = document.getElementById('suggestions'); const old = document.getElementById('suggestions');
if (old != null) old.remove(); if (old != null) old.remove();
if (items.length == 0) return;
suggestions = document.createElement('div'); if (items.length === 0) return;
console.log(items);
const suggestions = document.createElement('div');
suggestions.setAttribute('id', 'suggestions'); suggestions.setAttribute('id', 'suggestions');
suggestions.setAttribute('class', 'suggestions'); suggestions.setAttribute('class', 'suggestions');
@ -69,7 +74,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) {
}); });
suggestions.appendChild(item); suggestions.appendChild(item);
} }
document.getElementsByClassName('input')[0].appendChild(suggestions); queryBox.prepend(suggestions);
} }
req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value)); req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value));
@ -77,10 +82,15 @@ if(!window.matchMedia("(pointer: coarse)").matches) {
}, 250); }, 250);
} }
query.addEventListener("input", fetchSuggestions); query.addEventListener("input", fetchSuggestions);
query.addEventListener("click", e=> { var suggestions = document.getElementById('suggestions'); if (suggestions != null) suggestions.remove(); }); query.addEventListener("click", e=> {
const suggestions = document.getElementById('suggestions');
if (suggestions != null) {
suggestions.remove();
}
});
query.addEventListener("keydown", e => { query.addEventListener("keydown", e => {
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
var suggestions = document.getElementById('suggestions'); const suggestions = document.getElementById('suggestions');
if (suggestions != null) { if (suggestions != null) {
suggestions.childNodes[0].focus(); suggestions.childNodes[0].focus();
} }
@ -90,7 +100,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) {
e.preventDefault() e.preventDefault()
} }
else if (e.key === "Escape") { else if (e.key === "Escape") {
var suggestions = document.getElementById('suggestions'); const suggestions = document.getElementById('suggestions');
if (suggestions != null) { if (suggestions != null) {
suggestions.remove(); suggestions.remove();
} }
@ -99,3 +109,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) {
} }
}); });
} }
if(!window.matchMedia("(pointer: coarse)").matches) {
setupTypeahead();
}

View File

@ -1,4 +1,4 @@
<h2>Filters <button id="menu-close">X</button></h2> <h2>Filters</h2>
<ul> <ul>
{{#with removeJsOption}} {{#with removeJsOption}}
<li title="Exclude results with javascript" <li title="Exclude results with javascript"

View File

@ -1,15 +1,15 @@
<form action="/search" method="get"> <form action="/search" method="get" id="search-form">
<div id="search-box"> <div id="search-box">
<h1> <h1>
Search The Internet Search The Internet
</h1> </h1>
<input type="text" name="query" placeholder="Search..." value="{{query}}" autocomplete="off"> <div id="suggestions-anchor"></div>
<input type="text" id="query" name="query" placeholder="Search..." value="{{query}}">
<input type="hidden" name="js" value="{{js}}"> <input type="hidden" name="js" value="{{js}}">
<input type="hidden" name="adtech" value="{{adtech}}"> <input type="hidden" name="adtech" value="{{adtech}}">
<input type="hidden" name="profile" value="{{profile}}"> <input type="hidden" name="profile" value="{{profile}}">
<button type="submit">Search</button> <input type="submit" form="search-form" title="Execute Search" value="Search">
</div> </div>
</form> </form>
<!-- load the mobile customizations script early to avoid flicker --> <!-- load the mobile customizations script early to avoid flicker -->