Refactor autocomplete/suggestion behavior (front-end only)

The previous implementation of autocomplete/suggestions on the front end
resulted in a situation where input and keydown events were constantly
being added to the search input bar. This was refactored to set up the
events only once and process suggestion navigation and appending
suggestions separately with different functions.

This has been tested on both an Android simulator, as well as an Android
tablet and seems to work as expected.

Fixes #370
Fixes #629
main
Ben Busby 2022-06-07 10:57:26 -06:00
parent f9ff781df3
commit a9e1f0d1bc
No known key found for this signature in database
GPG Key ID: B9B7231E01D924A1
2 changed files with 107 additions and 98 deletions

View File

@ -1,4 +1,9 @@
const handleUserInput = searchBar => { let searchInput;
let currentFocus;
let originalSearch;
let autocompleteResults;
const handleUserInput = () => {
let xhrRequest = new XMLHttpRequest(); let xhrRequest = new XMLHttpRequest();
xhrRequest.open("POST", "autocomplete"); xhrRequest.open("POST", "autocomplete");
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
@ -9,48 +14,63 @@ const handleUserInput = searchBar => {
} }
// Fill autocomplete with fetched results // Fill autocomplete with fetched results
let autocompleteResults = JSON.parse(xhrRequest.responseText); autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
autocomplete(searchBar, autocompleteResults[1]); updateAutocompleteList();
}; };
xhrRequest.send('q=' + searchBar.value); xhrRequest.send('q=' + searchInput.value);
}; };
const autocomplete = (searchInput, autocompleteResults) => { const closeAllLists = el => {
let currentFocus; // Close all autocomplete suggestions
let originalSearch; let suggestions = document.getElementsByClassName("autocomplete-items");
for (let i = 0; i < suggestions.length; i++) {
searchInput.addEventListener("input", function () { if (el !== suggestions[i] && el !== searchInput) {
let autocompleteList, autocompleteItem, i, val = this.value; suggestions[i].parentNode.removeChild(suggestions[i]);
closeAllLists();
if (!val || !autocompleteResults) {
return false;
} }
}
};
const removeActive = suggestion => {
// Remove "autocomplete-active" class from previously active suggestion
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
const addActive = (suggestion) => {
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1; currentFocus = -1;
autocompleteList = document.createElement("div"); searchInput.value = originalSearch;
autocompleteList.setAttribute("id", this.id + "-autocomplete-list"); removeActive(suggestion);
autocompleteList.setAttribute("class", "autocomplete-items"); return;
this.parentNode.appendChild(autocompleteList); } else {
return;
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
closeAllLists();
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
} }
} }
});
searchInput.addEventListener("keydown", function (e) { removeActive(suggestion);
suggestion[currentFocus].classList.add("autocomplete-active");
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
let searchContent = suggestion[currentFocus].textContent;
if (searchContent.indexOf('(') > 0) {
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
} else {
searchInput.value = searchContent;
}
searchInput.focus();
};
const autocompleteInput = (e) => {
// Handle navigation between autocomplete suggestions
let suggestion = document.getElementById(this.id + "-autocomplete-list"); let suggestion = document.getElementById(this.id + "-autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div"); if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down if (e.keyCode === 40) { // down
@ -67,60 +87,46 @@ const autocomplete = (searchInput, autocompleteResults) => {
if (suggestion) suggestion[currentFocus].click(); if (suggestion) suggestion[currentFocus].click();
} }
} else { } else {
originalSearch = document.getElementById("search-bar").value; originalSearch = searchInput.value;
} }
}); };
const addActive = suggestion => { const updateAutocompleteList = () => {
let searchBar = document.getElementById("search-bar"); let autocompleteList, autocompleteItem, i;
let val = originalSearch;
closeAllLists();
if (!val || !autocompleteResults) {
return false;
}
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1; currentFocus = -1;
searchBar.value = originalSearch; autocompleteList = document.createElement("div");
removeActive(suggestion); autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
return; autocompleteList.setAttribute("class", "autocomplete-items");
} else { searchInput.parentNode.appendChild(autocompleteList);
return;
}
}
removeActive(suggestion); for (i = 0; i < autocompleteResults.length; i++) {
suggestion[currentFocus].classList.add("autocomplete-active"); if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator) autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
let searchContent = suggestion[currentFocus].textContent; autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
if (searchContent.indexOf('(') > 0) { autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
searchBar.value = searchContent.substring(0, searchContent.indexOf('(')); autocompleteItem.addEventListener("click", function () {
} else { searchInput.value = this.getElementsByTagName("input")[0].value;
searchBar.value = searchContent; closeAllLists();
} document.getElementById("search-form").submit();
});
searchBar.focus(); autocompleteList.appendChild(autocompleteItem);
};
const removeActive = suggestion => {
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
const closeAllLists = el => {
let suggestions = document.getElementsByClassName("autocomplete-items");
for (let i = 0; i < suggestions.length; i++) {
if (el !== suggestions[i] && el !== searchInput) {
suggestions[i].parentNode.removeChild(suggestions[i]);
} }
} }
}; };
// Close lists and search when user selects a suggestion document.addEventListener("DOMContentLoaded", function() {
searchInput = document.getElementById("search-bar");
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
document.addEventListener("click", function (e) { document.addEventListener("click", function (e) {
closeAllLists(e.target); closeAllLists(e.target);
}); });
}; });

View File

@ -2,6 +2,8 @@ const setupSearchLayout = () => {
// Setup search field // Setup search field
const searchBar = document.getElementById("search-bar"); const searchBar = document.getElementById("search-bar");
const searchBtn = document.getElementById("search-submit"); const searchBtn = document.getElementById("search-submit");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
// Automatically focus on search field // Automatically focus on search field
searchBar.focus(); searchBar.focus();
@ -11,8 +13,9 @@ const setupSearchLayout = () => {
if (event.keyCode === 13) { if (event.keyCode === 13) {
event.preventDefault(); event.preventDefault();
searchBtn.click(); searchBtn.click();
} else { } else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
handleUserInput(searchBar); searchValue = searchBar.value;
handleUserInput();
} }
}); });
}; };