(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');
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
const button = document.createElement('button');
button.setAttribute('id', 'mcfeast');
button.setAttribute('aria-controls', '#filters');
button.onclick = (event) => {
setDisplay(document.getElementById('filters'), 'block');
const filtersButton = document.createElement('button');
filtersButton.setAttribute('id', 'mcfeast');
filtersButton.setAttribute('aria-controls', '#filters');
filtersButton.onclick = (event) => {
// 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();
return false;
}
button.innerHTML = 'Filters';
document.getElementById('search-box').getElementsByTagName('h1')[0].append(button);
filtersButton.innerHTML = 'Filters';
document.getElementById('search-box').getElementsByTagName('h1')[0].append(filtersButton);
function setDisplay(element, value) {
element.style.display = value;
}
document.getElementById('menu-close').onclick = (event) => {
setDisplay(document.getElementById('filters'), 'none');
event.stopPropagation();
return false;
}
// To prevent the filter menu from being opened when the user hits enter on the search box, we need to add a keydown
// 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") {
const form = document.getElementById('search-form');
form.submit();
e.preventDefault();
}
});

View File

@ -272,6 +272,7 @@ section.cards {
margin-left: 1ch;
}
footer {
clear: both;
@ -348,7 +349,7 @@ footer {
padding: 0.5ch;
background-color: $fg-light;
display: grid;
grid-template-columns: max-content auto max-content;
grid-template-columns: max-content 0 auto max-content;
grid-gap: 0.5ch;
grid-auto-rows: minmax(1ch, auto);
width: 100%;
@ -366,6 +367,11 @@ footer {
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"] {
font-family: monospace;
font-size: 12pt;
@ -375,13 +381,42 @@ footer {
color: $fg-dark;
}
button[type="submit"] {
input[type="submit"] {
font-size: 12pt;
border: 1px solid $border-color;
background-color: $fg-light;
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 {
@ -519,6 +554,8 @@ footer {
padding: 0 0 0 0 !important;
max-width: 100%;
#suggestions-anchor { display: none; } // suggestions are not useful on mobile
.sidebar-narrow {
display: block; // fix for bizarre chrome rendering issue
}

View File

@ -1,86 +1,96 @@
if(!window.matchMedia("(pointer: coarse)").matches) {
query = document.getElementById('query');
function setupTypeahead() {
const query = document.getElementById('query');
query.setAttribute('autocomplete', 'off');
timer = null;
const queryBox = document.getElementById('suggestions-anchor');
let timer = null;
function fetchSuggestions(e) {
if (timer != null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
req = new XMLHttpRequest();
if (timer != null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
const req = new XMLHttpRequest();
req.onload = rsp => {
items = JSON.parse(req.responseText);
req.onload = rsp => {
let items = JSON.parse(req.responseText);
var old = document.getElementById('suggestions');
if (old != null) old.remove();
if (items.length == 0) return;
suggestions = document.createElement('div');
suggestions.setAttribute('id', 'suggestions');
suggestions.setAttribute('class', 'suggestions');
const old = document.getElementById('suggestions');
if (old != null) old.remove();
for (i=0;i<items.length;i++) {
item = document.createElement('a');
item.innerHTML=items[i];
item.setAttribute('href', '#')
if (items.length === 0) return;
console.log(items);
const suggestions = document.createElement('div');
suggestions.setAttribute('id', 'suggestions');
suggestions.setAttribute('class', 'suggestions');
for (i=0;i<items.length;i++) {
item = document.createElement('a');
item.innerHTML=items[i];
item.setAttribute('href', '#')
function suggestionClickHandler(e) {
query.value = e.target.text;
query.focus();
document.getElementById('suggestions').remove();
e.preventDefault()
}
item.addEventListener('click', suggestionClickHandler);
item.addEventListener('keydown', e=> {
if (e.key === "ArrowDown") {
if (e.target.nextElementSibling != null) {
e.target.nextElementSibling.focus();
}
function suggestionClickHandler(e) {
query.value = e.target.text;
query.focus();
document.getElementById('suggestions').remove();
e.preventDefault()
}
item.addEventListener('click', suggestionClickHandler);
item.addEventListener('keydown', e=> {
if (e.key === "ArrowDown") {
if (e.target.nextElementSibling != null) {
e.target.nextElementSibling.focus();
}
e.preventDefault()
else if (e.key === "ArrowUp") {
if (e.target.previousElementSibling != null) {
e.target.previousElementSibling.focus();
}
else if (e.key === "ArrowUp") {
if (e.target.previousElementSibling != null) {
e.target.previousElementSibling.focus();
}
else {
query.focus();
}
e.preventDefault()
}
else if (e.key === "Escape") {
var suggestions = document.getElementById('suggestions');
if (suggestions != null) {
suggestions.remove();
}
else {
query.focus();
e.preventDefault();
}
});
item.addEventListener('keypress', e=> {
if (e.key === "Enter") {
suggestionClickHandler(e);
e.preventDefault()
}
else if (e.key === "Escape") {
var suggestions = document.getElementById('suggestions');
if (suggestions != null) {
suggestions.remove();
}
});
suggestions.appendChild(item);
}
document.getElementsByClassName('input')[0].appendChild(suggestions);
query.focus();
e.preventDefault();
}
});
item.addEventListener('keypress', e=> {
if (e.key === "Enter") {
suggestionClickHandler(e);
}
});
suggestions.appendChild(item);
}
queryBox.prepend(suggestions);
}
req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value));
req.send();
}, 250);
}
req.open("GET", "/suggest/?partial="+encodeURIComponent(query.value));
req.send();
}, 250);
}
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 => {
if (e.key === "ArrowDown") {
var suggestions = document.getElementById('suggestions');
const suggestions = document.getElementById('suggestions');
if (suggestions != null) {
suggestions.childNodes[0].focus();
}
@ -90,7 +100,7 @@ if(!window.matchMedia("(pointer: coarse)").matches) {
e.preventDefault()
}
else if (e.key === "Escape") {
var suggestions = document.getElementById('suggestions');
const suggestions = document.getElementById('suggestions');
if (suggestions != null) {
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>
{{#with removeJsOption}}
<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">
<h1>
Search The Internet
</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="adtech" value="{{adtech}}">
<input type="hidden" name="profile" value="{{profile}}">
<button type="submit">Search</button>
<input type="submit" form="search-form" title="Execute Search" value="Search">
</div>
</form>
<!-- load the mobile customizations script early to avoid flicker -->