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 #629main
parent
f9ff781df3
commit
a9e1f0d1bc
|
@ -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,118 +14,119 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
currentFocus = -1;
|
const removeActive = suggestion => {
|
||||||
autocompleteList = document.createElement("div");
|
// Remove "autocomplete-active" class from previously active suggestion
|
||||||
autocompleteList.setAttribute("id", this.id + "-autocomplete-list");
|
for (let i = 0; i < suggestion.length; i++) {
|
||||||
autocompleteList.setAttribute("class", "autocomplete-items");
|
suggestion[i].classList.remove("autocomplete-active");
|
||||||
this.parentNode.appendChild(autocompleteList);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for (i = 0; i < autocompleteResults.length; i++) {
|
const addActive = (suggestion) => {
|
||||||
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
|
// Handle navigation outside of suggestion list
|
||||||
autocompleteItem = document.createElement("div");
|
if (!suggestion || !suggestion[currentFocus]) {
|
||||||
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
|
if (currentFocus >= suggestion.length) {
|
||||||
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
|
// Move selection back to the beginning
|
||||||
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
|
currentFocus = 0;
|
||||||
autocompleteItem.addEventListener("click", function () {
|
} else if (currentFocus < 0) {
|
||||||
searchInput.value = this.getElementsByTagName("input")[0].value;
|
// Retrieve original search and remove active suggestion selection
|
||||||
closeAllLists();
|
currentFocus = -1;
|
||||||
document.getElementById("search-form").submit();
|
searchInput.value = originalSearch;
|
||||||
});
|
removeActive(suggestion);
|
||||||
autocompleteList.appendChild(autocompleteItem);
|
return;
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
originalSearch = document.getElementById("search-bar").value;
|
return;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const addActive = suggestion => {
|
removeActive(suggestion);
|
||||||
let searchBar = document.getElementById("search-bar");
|
suggestion[currentFocus].classList.add("autocomplete-active");
|
||||||
|
|
||||||
// Handle navigation outside of suggestion list
|
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
|
||||||
if (!suggestion || !suggestion[currentFocus]) {
|
let searchContent = suggestion[currentFocus].textContent;
|
||||||
if (currentFocus >= suggestion.length) {
|
if (searchContent.indexOf('(') > 0) {
|
||||||
// Move selection back to the beginning
|
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
|
||||||
currentFocus = 0;
|
} else {
|
||||||
} else if (currentFocus < 0) {
|
searchInput.value = searchContent;
|
||||||
// Retrieve original search and remove active suggestion selection
|
}
|
||||||
currentFocus = -1;
|
|
||||||
searchBar.value = originalSearch;
|
searchInput.focus();
|
||||||
removeActive(suggestion);
|
};
|
||||||
return;
|
|
||||||
} else {
|
const autocompleteInput = (e) => {
|
||||||
return;
|
// 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeActive(suggestion);
|
const updateAutocompleteList = () => {
|
||||||
suggestion[currentFocus].classList.add("autocomplete-active");
|
let autocompleteList, autocompleteItem, i;
|
||||||
|
let val = originalSearch;
|
||||||
|
closeAllLists();
|
||||||
|
|
||||||
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
|
if (!val || !autocompleteResults) {
|
||||||
let searchContent = suggestion[currentFocus].textContent;
|
return false;
|
||||||
if (searchContent.indexOf('(') > 0) {
|
}
|
||||||
searchBar.value = searchContent.substring(0, searchContent.indexOf('('));
|
|
||||||
} else {
|
currentFocus = -1;
|
||||||
searchBar.value = searchContent;
|
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 = "<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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
searchBar.focus();
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
};
|
searchInput = document.getElementById("search-bar");
|
||||||
|
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
|
||||||
|
|
||||||
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("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
closeAllLists(e.target);
|
closeAllLists(e.target);
|
||||||
});
|
});
|
||||||
};
|
});
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue