/** * Initialises a selection based on existing HTML elements. * For HTML elements are required: * - A text input with id $idPrefix * - A list with id ${idPrefix}-list * - An element shown if no results are available with id ${idPrefix}-noresults * - A button to clear the input with id ${idPrefix}-clear * * @param {string} idPrefix - The id prefix of the four HTML elements. * @param {async function} fetchCandidates - The async function to retrieve the selectable candidates. * @param {function} fmtCandidate - The function to format the displayed HTML for each candidate. * @param {function} onSelection - The function to call when a candidate is selected. */ async function initSelection(idPrefix, fetchCandidates, fmtCandidate, onSelection) { const sel = document.getElementById(idPrefix); const list = document.getElementById(`${idPrefix}-list`); const noResults = document.getElementById(`${idPrefix}-noresults`); const clearBtn = document.getElementById(`${idPrefix}-clear`); const loader = document.getElementById(`${idPrefix}-loader`); loader.style.display = "unset"; const candidates = await fetchCandidates(); loader.style.display = "none"; const items = []; let currentFocus = -1; let visibleItems = []; function setActive() { visibleItems.forEach((item) => item.classList.remove("active")); if (currentFocus >= 0 && visibleItems[currentFocus]) { visibleItems[currentFocus].classList.add("active"); } } for (const candidate of candidates) { const item = document.createElement("li"); item.candidate = candidate; item.innerHTML = fmtCandidate(candidate); item.style.display = "none"; item.addEventListener("click", function () { sel.value = ""; sel.dispatchEvent(new Event("keyup")); onSelection(candidate); }); list.appendChild(item); items.push(item); } sel.addEventListener("keyup", function (e) { if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) return; const filter = sel.value.toLowerCase(); visibleItems = []; for (const item of items) { const itemText = item.textContent.trim().toLowerCase(); if (itemText.indexOf(filter) > -1 && filter !== "") { item.style.display = ""; visibleItems.push(item); // Highlight matched text const regex = new RegExp(`(${filter})`, "gi"); item.innerHTML = item.textContent.replace( regex, "$1", ); } else { item.style.display = "none"; } } // Show the clear button if there is an input clearBtn.style.display = sel.value ? "block" : "none"; // Select the first or none if there are no items visible currentFocus = visibleItems.length ? 0 : -1; setActive(visibleItems); noResults.style.display = visibleItems.length || filter === "" ? "none" : "block"; }); clearBtn.addEventListener("click", function () { sel.value = ""; sel.dispatchEvent(new Event("keyup")); // reset list sel.focus(); }); sel.addEventListener("keydown", function (e) { if (e.key === "ArrowDown" || e.key === "ArrowUp") { if (e.key === "ArrowDown") currentFocus = (currentFocus + 1) % visibleItems.length; else currentFocus = (currentFocus - 1 + visibleItems.length) % visibleItems.length; setActive(); e.preventDefault(); } else if (e.key === "Enter") { if (currentFocus > -1 && visibleItems[currentFocus]) { const visibleItem = visibleItems[currentFocus]; sel.value = ""; sel.dispatchEvent(new Event("keyup")); onSelection(visibleItem.candidate); } e.preventDefault(); } }); } export { initSelection };