From 4882c5ca3a65075e03595588ef3780a36bc74b15 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Sat, 13 Jun 2026 13:04:07 +0200 Subject: move generic js into root --- js/selection.js | 167 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 js/selection.js (limited to 'js/selection.js') diff --git a/js/selection.js b/js/selection.js new file mode 100644 index 0000000..a1da244 --- /dev/null +++ b/js/selection.js @@ -0,0 +1,167 @@ +/** + * 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 candidates = await fetchCandidates(); + + let 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) { + let 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(); + } + }); +} + +function pointClickHandler(color) { + const textareaDiv = document.getElementById("student-info-point-reason-container"); + const textarea = document.getElementById("student-info-point-reason"); + const submitBtn = document.getElementById("student-info-point-reason-submit"); + + // TODO(muhq): Prefill the textarea with common reasons + switch (color) { + case "green": + textarea.style.background = "rgb(from green r g b / 20%)" + textarea.value = ""; + break; + case "orange": + textarea.style.background = "rgb(from orange r g b / 20%)" + textarea.value = ""; + break; + case "red": + textarea.style.background = "rgb(from red r g b / 20%)" + textarea.value = ""; + break; + } + textareaDiv.style.display = "block"; + textarea.focus(); + + // Can not use string interpolation because of Moodle's firstname filter. + const coach = firstname + " " + lastname; + + let submit = function() { + let change; + switch (color) { + case "green": + change = GraduationChange.newGreenPointChange(student, textarea.value, coach); + break; + case "orange": + change = GraduationChange.newOrangePointChange(student, textarea.value, coach); + break; + case "red": + change = GraduationChange.newRedPointChange(student, textarea.value, coach); + break; + default: + alert("Invalid color"); + break; + } + submitGraduationChange(student, change); + textareaDiv.style.display = "none"; + updateStudentInfo(); + } + // Use onEVENT to override any previous handler + textarea.onkeydown = function(e) { + if (e.shiftKey && e.key === "Enter") { + submit(); + } + if (e.key === "Escape") { + textareaDiv.style.display = "none"; + } + }; + submitBtn.onclick = submit; +} + +export { initSelection }; -- cgit v1.2.3