aboutsummaryrefslogtreecommitdiff
path: root/src/selection.js
blob: bfaebc00b16a4c269f9ee89c152ea52d166e8bba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
 * 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,
					"<u class='student-selection-list-highlight'>$1</u>"
				);
			} 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 };