philomena/assets/js/autocomplete.js

195 lines
6.4 KiB
JavaScript
Raw Normal View History

2019-10-05 02:09:52 +02:00
/**
* Autocomplete.
*/
2021-12-27 01:16:21 +01:00
import { LocalAutocompleter } from 'utils/local-autocompleter';
import { handleError } from 'utils/requests';
2019-10-05 02:09:52 +02:00
const cache = {};
let inputField, originalTerm;
function removeParent() {
const parent = document.querySelector('.autocomplete');
if (parent) parent.parentNode.removeChild(parent);
}
function removeSelected() {
const selected = document.querySelector('.autocomplete__item--selected');
if (selected) selected.classList.remove('autocomplete__item--selected');
}
function changeSelected(firstOrLast, current, sibling) {
if (current && sibling) { // if the currently selected item has a sibling, move selection to it
current.classList.remove('autocomplete__item--selected');
sibling.classList.add('autocomplete__item--selected');
}
else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term
inputField.value = originalTerm;
removeSelected();
}
else if (firstOrLast) { // if no item in the list is selected, select the first or last
firstOrLast.classList.add('autocomplete__item--selected');
}
}
function keydownHandler(event) {
const selected = document.querySelector('.autocomplete__item--selected'),
firstItem = document.querySelector('.autocomplete__item:first-of-type'),
lastItem = document.querySelector('.autocomplete__item:last-of-type');
if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown
const newSelected = document.querySelector('.autocomplete__item--selected');
if (newSelected) inputField.value = newSelected.dataset.value;
event.preventDefault();
}
}
function createItem(list, suggestion) {
const item = document.createElement('li');
item.className = 'autocomplete__item';
item.textContent = suggestion.label;
item.dataset.value = suggestion.value;
item.addEventListener('mouseover', () => {
removeSelected();
item.classList.add('autocomplete__item--selected');
});
item.addEventListener('mouseout', () => {
removeSelected();
});
item.addEventListener('click', () => {
inputField.value = item.dataset.value;
2019-10-05 02:09:52 +02:00
inputField.dispatchEvent(
new CustomEvent('autocomplete', {
detail: {
type: 'click',
label: suggestion.label,
value: suggestion.value,
}
})
);
});
list.appendChild(item);
}
function createList(suggestions) {
const parent = document.querySelector('.autocomplete'),
list = document.createElement('ul');
list.className = 'autocomplete__list';
suggestions.forEach(suggestion => createItem(list, suggestion));
parent.appendChild(list);
}
function createParent() {
const parent = document.createElement('div');
parent.className = 'autocomplete';
// Position the parent below the inputfield
parent.style.position = 'absolute';
parent.style.left = `${inputField.offsetLeft}px`;
// Take the inputfield offset, add its height and subtract the amount by which the parent element has scrolled
parent.style.top = `${inputField.offsetTop + inputField.offsetHeight - inputField.parentNode.scrollTop}px`;
// We append the parent at the end of body
document.body.appendChild(parent);
}
function showAutocomplete(suggestions, fetchedTerm, targetInput) {
2019-10-05 02:09:52 +02:00
// Remove old autocomplete suggestions
removeParent();
// Save suggestions in cache
cache[fetchedTerm] = suggestions;
2019-10-05 02:09:52 +02:00
// If the input target is not empty, still visible, and suggestions were found
if (targetInput.value && targetInput.style.display !== 'none' && suggestions.length) {
createParent();
createList(suggestions);
inputField.addEventListener('keydown', keydownHandler);
}
}
function getSuggestions(term) {
return fetch(`${inputField.dataset.acSource}${term}`).then(response => response.json());
2019-10-05 02:09:52 +02:00
}
function listenAutocomplete() {
let timeout;
2021-12-27 01:16:21 +01:00
/** @type {LocalAutocompleter} */
let localAc = null;
let localFetched = false;
document.addEventListener('focusin', fetchLocalAutocomplete);
2019-10-05 02:09:52 +02:00
document.addEventListener('input', event => {
removeParent();
2021-12-27 01:16:21 +01:00
fetchLocalAutocomplete(event);
window.clearTimeout(timeout);
2021-12-27 01:16:21 +01:00
if (localAc !== null && 'ac' in event.target.dataset) {
inputField = event.target;
2021-12-30 01:52:15 +01:00
originalTerm = `${inputField.value}`.toLowerCase();
2021-12-27 01:16:21 +01:00
2021-12-30 01:52:15 +01:00
const suggestions = localAc.topK(originalTerm, 5).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name }));
if (suggestions.length) {
return showAutocomplete(suggestions, originalTerm, event.target);
}
2021-12-27 01:16:21 +01:00
}
2019-10-05 02:09:52 +02:00
// Use a timeout to delay requests until the user has stopped typing
timeout = window.setTimeout(() => {
inputField = event.target;
originalTerm = inputField.value;
const fetchedTerm = inputField.value;
const {ac, acMinLength} = inputField.dataset;
2019-10-05 02:09:52 +02:00
if (ac && (fetchedTerm.length >= acMinLength)) {
if (cache[fetchedTerm]) {
2021-11-17 02:20:55 +01:00
showAutocomplete(cache[fetchedTerm], fetchedTerm, event.target);
2019-10-05 02:09:52 +02:00
}
else {
// inputField could get overwritten while the suggestions are being fetched - use event.target
getSuggestions(fetchedTerm).then(suggestions => {
if (fetchedTerm === event.target.value) {
showAutocomplete(suggestions, fetchedTerm, event.target);
}
});
2019-10-05 02:09:52 +02:00
}
}
}, 300);
});
// If there's a click outside the inputField, remove autocomplete
document.addEventListener('click', event => {
if (event.target && event.target !== inputField) removeParent();
});
2021-12-27 01:16:21 +01:00
function fetchLocalAutocomplete(event) {
if (!localFetched && event.target.dataset && 'ac' in event.target.dataset) {
2023-03-30 15:45:14 +02:00
const now = new Date();
const cacheKey = `${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}`;
2021-12-27 01:16:21 +01:00
localFetched = true;
2023-03-30 15:45:14 +02:00
fetch(`/autocomplete/compiled?vsn=2&key=${cacheKey}`, { credentials: 'omit', cache: 'force-cache' })
2021-12-27 01:16:21 +01:00
.then(handleError)
.then(resp => resp.arrayBuffer())
.then(buf => localAc = new LocalAutocompleter(buf));
}
}
2019-10-05 02:09:52 +02:00
}
export { listenAutocomplete };