aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cockpit/js/cockpit.js101
-rw-r--r--cockpit/js/selection.js167
2 files changed, 175 insertions, 93 deletions
diff --git a/cockpit/js/cockpit.js b/cockpit/js/cockpit.js
index 8041e77..c6524d6 100644
--- a/cockpit/js/cockpit.js
+++ b/cockpit/js/cockpit.js
@@ -11,6 +11,7 @@ import {
import { fetchStudent, fetchStudents } from "./student-ENV.js";
import { isTeacher as getIsTeacher } from "../../utils-ENV.js";
+import { initSelection } from "./selection.js";
let isTeacher;
let firstname;
@@ -37,99 +38,13 @@ async function loadStudent(studentToLoad) {
updateStudentGraduation();
}
-async function initStudentSelection() {
- const sel = document.getElementById("student-selection");
- const list = document.getElementById('student-selection-list');
- const noResults = document.getElementById('student-selection-noresults');
- const clearBtn = document.getElementById('student-selection-clear');
-
- const students = await fetchStudents();
-
- 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 student of students) {
- let item = document.createElement("li");
- item.student = student;
- item.innerHTML = `${student.name} ${student.group}`;
- item.style.display = 'none';
-
- item.addEventListener('click', function() {
- sel.value = "";
- sel.dispatchEvent(new Event('keyup'));
- loadStudent(student);
- });
-
- 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'));
- loadStudent(visibleItem.student);
- }
- e.preventDefault();
- }
- });
+function initStudentSelection() {
+ initSelection(
+ "student-selection",
+ fetchStudents,
+ (student) => `${student.name} ${student.group}`,
+ loadStudent
+ );
}
function pointClickHandler(color) {
diff --git a/cockpit/js/selection.js b/cockpit/js/selection.js
new file mode 100644
index 0000000..a1da244
--- /dev/null
+++ b/cockpit/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,
+ "<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();
+ }
+ });
+}
+
+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 };