From a9e1f0d1bc679201d6d0f0ff97b0d9172b56b537 Mon Sep 17 00:00:00 2001 From: Ben Busby Date: Tue, 7 Jun 2022 10:57:26 -0600 Subject: [PATCH] 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 --- app/static/js/autocomplete.js | 198 +++++++++++++++++----------------- app/static/js/controller.js | 7 +- 2 files changed, 107 insertions(+), 98 deletions(-) diff --git a/app/static/js/autocomplete.js b/app/static/js/autocomplete.js index 702ebc4..87d0c42 100644 --- a/app/static/js/autocomplete.js +++ b/app/static/js/autocomplete.js @@ -1,4 +1,9 @@ -const handleUserInput = searchBar => { +let searchInput; +let currentFocus; +let originalSearch; +let autocompleteResults; + +const handleUserInput = () => { let xhrRequest = new XMLHttpRequest(); xhrRequest.open("POST", "autocomplete"); xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); @@ -9,118 +14,119 @@ const handleUserInput = searchBar => { } // Fill autocomplete with fetched results - let autocompleteResults = JSON.parse(xhrRequest.responseText); - autocomplete(searchBar, autocompleteResults[1]); + autocompleteResults = JSON.parse(xhrRequest.responseText)[1]; + updateAutocompleteList(); }; - xhrRequest.send('q=' + searchBar.value); + xhrRequest.send('q=' + searchInput.value); }; -const autocomplete = (searchInput, autocompleteResults) => { - let currentFocus; - let originalSearch; - - searchInput.addEventListener("input", function () { - let autocompleteList, autocompleteItem, i, val = this.value; - closeAllLists(); - - if (!val || !autocompleteResults) { - return false; +const closeAllLists = el => { + // Close all autocomplete suggestions + 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]); } + } +}; - currentFocus = -1; - autocompleteList = document.createElement("div"); - autocompleteList.setAttribute("id", this.id + "-autocomplete-list"); - autocompleteList.setAttribute("class", "autocomplete-items"); - this.parentNode.appendChild(autocompleteList); - - for (i = 0; i < autocompleteResults.length; i++) { - if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) { - autocompleteItem = document.createElement("div"); - autocompleteItem.innerHTML = "" + autocompleteResults[i].substr(0, val.length) + ""; - autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length); - autocompleteItem.innerHTML += ""; - autocompleteItem.addEventListener("click", function () { - searchInput.value = this.getElementsByTagName("input")[0].value; - closeAllLists(); - document.getElementById("search-form").submit(); - }); - autocompleteList.appendChild(autocompleteItem); - } - } - }); +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"); + } +}; - searchInput.addEventListener("keydown", function (e) { - let suggestion = document.getElementById(this.id + "-autocomplete-list"); - if (suggestion) suggestion = suggestion.getElementsByTagName("div"); - if (e.keyCode === 40) { // down - e.preventDefault(); - currentFocus++; - addActive(suggestion); - } else if (e.keyCode === 38) { //up - e.preventDefault(); - currentFocus--; - addActive(suggestion); - } else if (e.keyCode === 13) { // enter - e.preventDefault(); - if (currentFocus > -1) { - if (suggestion) suggestion[currentFocus].click(); - } +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; + searchInput.value = originalSearch; + removeActive(suggestion); + return; } else { - originalSearch = document.getElementById("search-bar").value; - } - }); - - const addActive = suggestion => { - let searchBar = document.getElementById("search-bar"); - - // 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; - searchBar.value = originalSearch; - removeActive(suggestion); - return; - } else { - return; - } + return; } + } - removeActive(suggestion); - suggestion[currentFocus].classList.add("autocomplete-active"); + 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) { - searchBar.value = searchContent.substring(0, searchContent.indexOf('(')); - } else { - searchBar.value = searchContent; - } + // 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; + } - searchBar.focus(); - }; + searchInput.focus(); +}; - const removeActive = suggestion => { - for (let i = 0; i < suggestion.length; i++) { - suggestion[i].classList.remove("autocomplete-active"); +const autocompleteInput = (e) => { + // Handle navigation between autocomplete suggestions + let suggestion = document.getElementById(this.id + "-autocomplete-list"); + if (suggestion) suggestion = suggestion.getElementsByTagName("div"); + if (e.keyCode === 40) { // down + e.preventDefault(); + currentFocus++; + addActive(suggestion); + } else if (e.keyCode === 38) { //up + e.preventDefault(); + currentFocus--; + addActive(suggestion); + } else if (e.keyCode === 13) { // enter + e.preventDefault(); + if (currentFocus > -1) { + if (suggestion) suggestion[currentFocus].click(); } - }; + } else { + originalSearch = searchInput.value; + } +}; - 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]); - } +const updateAutocompleteList = () => { + let autocompleteList, autocompleteItem, i; + let val = originalSearch; + closeAllLists(); + + if (!val || !autocompleteResults) { + return false; + } + + currentFocus = -1; + autocompleteList = document.createElement("div"); + autocompleteList.setAttribute("id", this.id + "-autocomplete-list"); + autocompleteList.setAttribute("class", "autocomplete-items"); + searchInput.parentNode.appendChild(autocompleteList); + + for (i = 0; i < autocompleteResults.length; i++) { + if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) { + autocompleteItem = document.createElement("div"); + autocompleteItem.innerHTML = "" + autocompleteResults[i].substr(0, val.length) + ""; + autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length); + autocompleteItem.innerHTML += ""; + autocompleteItem.addEventListener("click", function () { + searchInput.value = this.getElementsByTagName("input")[0].value; + closeAllLists(); + document.getElementById("search-form").submit(); + }); + autocompleteList.appendChild(autocompleteItem); } - }; + } +}; + +document.addEventListener("DOMContentLoaded", function() { + searchInput = document.getElementById("search-bar"); + searchInput.addEventListener("keydown", (event) => autocompleteInput(event)); - // Close lists and search when user selects a suggestion document.addEventListener("click", function (e) { closeAllLists(e.target); }); -}; +}); \ No newline at end of file diff --git a/app/static/js/controller.js b/app/static/js/controller.js index 05b68ec..9f05e16 100644 --- a/app/static/js/controller.js +++ b/app/static/js/controller.js @@ -2,6 +2,8 @@ const setupSearchLayout = () => { // Setup search field const searchBar = document.getElementById("search-bar"); const searchBtn = document.getElementById("search-submit"); + const arrowKeys = [37, 38, 39, 40]; + let searchValue = searchBar.value; // Automatically focus on search field searchBar.focus(); @@ -11,8 +13,9 @@ const setupSearchLayout = () => { if (event.keyCode === 13) { event.preventDefault(); searchBtn.click(); - } else { - handleUserInput(searchBar); + } else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) { + searchValue = searchBar.value; + handleUserInput(); } }); };