From 252ee412266b809830996e2588b16dd8e93379c0 Mon Sep 17 00:00:00 2001 From: styrix560 Date: Mon, 15 Dec 2025 19:57:45 +0100 Subject: [PATCH 01/10] Started refactor --- evap/static/ts/src/student-vote.ts | 229 ++++++++++++++++++++++- evap/student/templates/student_vote.html | 217 --------------------- 2 files changed, 226 insertions(+), 220 deletions(-) diff --git a/evap/static/ts/src/student-vote.ts b/evap/static/ts/src/student-vote.ts index 0a2c6b47ef..d2ee442d4e 100644 --- a/evap/static/ts/src/student-vote.ts +++ b/evap/static/ts/src/student-vote.ts @@ -1,4 +1,8 @@ import { selectOrError } from "./utils.js"; +import { AutoFormSaver } from "./auto-form-saver.js"; +import { CSRF_HEADERS } from "./csrf-utils.js"; +import { initTextAnswerWarnings } from "./text-answer-warnings.js"; +import { assert } from "./utils.js"; function isInvisible(el: Element): boolean { if (getComputedStyle(el).display === "none") { @@ -129,11 +133,11 @@ studentForm.addEventListener("keydown", (e: KeyboardEvent) => { // Just giving back control to the browser here doesn't work, because // it would navigate backwards through the controls of the current row. disableFocusHandler = true; - selectables[0].focus({ preventScroll: true }); + selectables[0].focus({preventScroll: true}); return; } else if (nextRowIndex === rows.length) { // User wants to tab out behind the form area - selectables[selectables.length - 1].focus({ preventScroll: true }); + selectables[selectables.length - 1].focus({preventScroll: true}); return; } } while (isInvisible(rows[nextRowIndex]) || !hasTabbingTarget(rows[nextRowIndex])); @@ -160,7 +164,7 @@ function findCorrectInputInRow(row: HTMLElement) { } function fancyFocus(element: HTMLElement) { - element.focus({ preventScroll: true }); + element.focus({preventScroll: true}); element.scrollIntoView({ behavior: "smooth", block: "center", @@ -176,3 +180,222 @@ function scrollToFirstChoiceError() { fancyFocus(findCorrectInputInRow(tabRow)); } } + + +const lastSavedStorageKey = "student-vote-last-saved-at-{{ evaluation.id }}-{{ request.user.id }}"; +const textResultsPublishConfirmation = { + top: document.querySelector("#text_results_publish_confirmation_top")!, + bottom: document.querySelector("#text_results_publish_confirmation_bottom")!, + bottomCard: document.querySelector("#bottom_text_results_publish_confirmation_card"), +}; + +// Ensure that selected questionnaire language is saved and loaded +const languageStorageKey = "student-vote-language-{{ evaluation.id }}-{{ request.user.id }}"; +const params = new URLSearchParams(document.location.search); +const currentlySelectedLanguage = "{{ evaluation_language|escapejs }}"; +const savedLanguage = localStorage.getItem(languageStorageKey); + +if (params.get("language")) { + localStorage.setItem(languageStorageKey, currentlySelectedLanguage); +} else if (savedLanguage && savedLanguage !== currentlySelectedLanguage) { + params.set("language", savedLanguage); + document.location.search = params.toString(); +} + +const formSaver = new AutoFormSaver(document.getElementById("student-vote-form") as HTMLFormElement, { + customKeySuffix: "[user={{ request.user.id }}]", // don't load data for other users + onRestore: function () { + // restore publish confirmation state + if (textResultsPublishConfirmation.bottomCard) { + updateTextResultsPublishConfirmation(); + } + + // show all non-empty additional text answer fields + document.querySelectorAll(".row-question .collapse textarea").forEach(el => { + if (el.value.length !== 0) { + const button = el.closest(".row")!.getElementsByClassName("btn-textanswer")[0] as HTMLButtonElement; + button.click(); + } + }); + }, + onSave: function () { + const timeNow = new Date(); + localStorage.setItem(lastSavedStorageKey, timeNow.toString()); + }, +}); + +function updateLastSavedLabel() { + const timeNow = new Date(); + const lastSavedLabel = document.getElementById('last-saved')!; + const lastSavedStorageValue = localStorage.getItem(lastSavedStorageKey); + if (lastSavedStorageValue !== null) { + const lastSavedDate = new Date(lastSavedStorageValue); + const delta = Math.round((timeNow.getTime() - lastSavedDate.getTime()) / 1000); + const languageCode = "{{ LANGUAGE_CODE }}"; + const relativeTimeFormat = new Intl.RelativeTimeFormat(languageCode); + let timeStamp; + if (delta < 3) { + timeStamp = "{% translate 'just now' %}"; + } else if (delta < 10) { + timeStamp = "{% translate 'less than 10 seconds ago' %}"; + } else if (delta < 30) { + timeStamp = "{% translate 'less than 30 seconds ago' %}"; + } else if (delta < 60) { + timeStamp = "{% translate 'less than 1 minute ago' %}"; + } else if (delta < 60 * 30) { + timeStamp = relativeTimeFormat.format(-Math.round(delta / 60), 'minutes'); + } else if (delta < 60 * 60 * 12) { + timeStamp = padWithLeadingZeros(lastSavedDate.getHours()) + ":" + padWithLeadingZeros(lastSavedDate.getMinutes()); + } else { + timeStamp = lastSavedDate.getFullYear().toString() + "-" + + padWithLeadingZeros(lastSavedDate.getMonth() + 1) + "-" + + padWithLeadingZeros(lastSavedDate.getDate()) + " " + + padWithLeadingZeros(lastSavedDate.getHours()) + ":" + + padWithLeadingZeros(lastSavedDate.getMinutes()); + } + lastSavedLabel.innerText = "{% translate 'Last saved locally' %}: " + timeStamp; + } else { + lastSavedLabel.innerText = "{% translate 'Could not save your information locally' %}"; + } +} + +function padWithLeadingZeros(number: number) { + return number.toString().padStart(2, "0"); +} + +// save all data after loading the page +// (the data gets deleted every time the form is submitted, i.e. also when the form had errors and is displayed again) +formSaver.saveAllData(); + +// Initialize lastSavedLabel and update it every second +updateLastSavedLabel(); +setInterval(updateLastSavedLabel, 1000); + +const textAnswerWarnings = document.getElementById("text-answer-warnings") as HTMLTextAreaElement; +initTextAnswerWarnings(document.querySelectorAll("#student-vote-form textarea"), JSON.parse(textAnswerWarnings.textContent) as string[][]); + +const form = document.getElementById('student-vote-form') as HTMLFormElement; +const submitListener = (event: Event) => { + event.preventDefault(); // don't use the default submission + const submitButton = document.getElementById('vote-submit-btn') as HTMLButtonElement; + const originalText = submitButton.innerText; + + submitButton.innerText = "{% translate 'Submitting...' %}"; + submitButton.disabled = true; + + fetch(form.action, { + body: new FormData(form), + headers: CSRF_HEADERS, + method: form.method, + }).then(response => { + assert(response.ok); + return response.text(); + }).then(response_text => { + if (response_text === "{{ success_magic_string }}") { + formSaver.releaseData(); + window.location.replace("{{ success_redirect_url }}"); + } else { + // resubmit without this handler to show the site with the form errors + form.removeEventListener("submit", submitListener); + form.requestSubmit(); + } + }).catch((_: unknown) => { + // show a warning if the post isn't successful + document.getElementById('submit-error-warning')!.classList.remove("d-none"); + submitButton.innerText = originalText; + submitButton.disabled = false; + }); +}; +form.addEventListener("submit", submitListener); + +function clearChoiceError(voteButton: HTMLInputElement) { + voteButton.closest(".row")!.querySelectorAll(".choice-error").forEach(highlightedElement => { + highlightedElement.classList.remove("choice-error"); + }); +} + +document.querySelectorAll("[data-mark-no-answers-for]").forEach(button => { + const contributorId = button.dataset.markNoAnswersFor!; + const voteArea = document.getElementById(`vote-area-${contributorId}`)!; + const collapseToggle = voteArea.closest(".collapsible")!.querySelector(".collapse-toggle")!; + + button.addEventListener("click", () => { + voteArea.querySelectorAll(".vote-inputs [type=radio][value='6']").forEach(radioInput => { + radioInput.checked = true; + clearChoiceError(radioInput); + }); + + formSaver.saveAllData(); + + // hide questionnaire for contributor + const voteAreaCollapse = bootstrap.Collapse.getOrCreateInstance(voteArea); + voteAreaCollapse.hide(); + collapseToggle.classList.add("tab-selectable"); + + // Disable this button, until user changes a value + button.classList.remove("tab-selectable"); + button.disabled = true; + }); + + voteArea.querySelectorAll(".vote-inputs [type=radio]:not([value='6'])") + .forEach(radioInput => { + radioInput.addEventListener("click", () => { + collapseToggle.classList.remove("tab-selectable"); + button.classList.add("tab-selectable"); + button.disabled = false; + }); + }); + + collapseToggle.addEventListener("click", () => { + if (button.classList.contains("tab-selectable")) { + collapseToggle.classList.remove("tab-selectable"); + } + }); +}); + +// remove error highlighting when an answer was selected +document.querySelectorAll(".vote-btn.choice-error").forEach(voteButton => { + voteButton.addEventListener("click", () => clearChoiceError(voteButton)); + const actualInput = document.getElementById(voteButton.attributes.for.value)!; + actualInput.addEventListener("click", () => clearChoiceError(voteButton)); +}); + +document.querySelectorAll(".btn-textanswer").forEach(textanswerButton => { + const textfieldClass = textanswerButton.dataset.bsTarget!; + const textfield = textanswerButton.closest(".row")!.querySelector(textfieldClass + " textarea")!; + textanswerButton.addEventListener("click", () => { + // focus textarea when opening the collapsed area + const isOpening = textanswerButton.classList.contains("collapsed"); + if (isOpening) { + requestAnimationFrame(() => { + textfield.focus(); + }); + } + }); + textfield.addEventListener("input", () => { + if (textfield.value.trim().length !== 0) { + textanswerButton.classList.add("has-contents"); + } else { + textanswerButton.classList.remove("has-contents"); + } + }); + textfield.dispatchEvent(new Event("input")); +}); + +// handle text_results_publish_confirmation checkbox changes +function updateTextResultsPublishConfirmation() { + const isChecked = textResultsPublishConfirmation.top.checked; + textResultsPublishConfirmation.bottom.checked = isChecked; + textResultsPublishConfirmation.bottomCard?.classList.toggle("d-none", isChecked); +} + +if (textResultsPublishConfirmation.bottomCard) { + textResultsPublishConfirmation.top.addEventListener("change", updateTextResultsPublishConfirmation); + textResultsPublishConfirmation.bottom.addEventListener("change", () => { + // The top checkbox should only be visually checked without triggering the change event, + // which would hide the bottom card. + // To keep the top checkbox checked (after a reload or submit), save the form manually. + textResultsPublishConfirmation.top.checked = textResultsPublishConfirmation.bottom.checked; + formSaver.saveAllData(); + }); +} \ No newline at end of file diff --git a/evap/student/templates/student_vote.html b/evap/student/templates/student_vote.html index 46d49b4c35..0cea1159d7 100644 --- a/evap/student/templates/student_vote.html +++ b/evap/student/templates/student_vote.html @@ -231,223 +231,6 @@

{% if not preview %} {{ text_answer_warnings|text_answer_warning_trigger_strings|json_script:'text-answer-warnings' }} - {% endif %} {% endblock %} From 2ee79184eaf5914abe3e0cf7aeecafa3cebbba82 Mon Sep 17 00:00:00 2001 From: styrix560 Date: Mon, 15 Dec 2025 20:40:21 +0100 Subject: [PATCH 02/10] Replace django template magic --- evap/static/ts/src/student-vote.ts | 26 ++++++++++++++++-------- evap/student/templates/student_vote.html | 10 ++++++++- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/evap/static/ts/src/student-vote.ts b/evap/static/ts/src/student-vote.ts index d2ee442d4e..f13f1edaad 100644 --- a/evap/static/ts/src/student-vote.ts +++ b/evap/static/ts/src/student-vote.ts @@ -4,6 +4,10 @@ import { CSRF_HEADERS } from "./csrf-utils.js"; import { initTextAnswerWarnings } from "./text-answer-warnings.js"; import { assert } from "./utils.js"; +// TODO: templates +// TODO: bootstrap +// TODO: for attribute on Button + function isInvisible(el: Element): boolean { if (getComputedStyle(el).display === "none") { return true; @@ -182,7 +186,10 @@ function scrollToFirstChoiceError() { } -const lastSavedStorageKey = "student-vote-last-saved-at-{{ evaluation.id }}-{{ request.user.id }}"; +const dataElement = document.querySelector(".dataElement")!; +const evaluationId = dataElement.dataset.evaluation_id!; +const requestUserId = dataElement.dataset.request_user_id; +const languageStorageKey = `student-vote-last-saved-at-${evaluationId}-${requestUserId}`; const textResultsPublishConfirmation = { top: document.querySelector("#text_results_publish_confirmation_top")!, bottom: document.querySelector("#text_results_publish_confirmation_bottom")!, @@ -190,9 +197,8 @@ const textResultsPublishConfirmation = { }; // Ensure that selected questionnaire language is saved and loaded -const languageStorageKey = "student-vote-language-{{ evaluation.id }}-{{ request.user.id }}"; const params = new URLSearchParams(document.location.search); -const currentlySelectedLanguage = "{{ evaluation_language|escapejs }}"; +const currentlySelectedLanguage = dataElement.dataset.evaluation_language!; const savedLanguage = localStorage.getItem(languageStorageKey); if (params.get("language")) { @@ -203,7 +209,7 @@ if (params.get("language")) { } const formSaver = new AutoFormSaver(document.getElementById("student-vote-form") as HTMLFormElement, { - customKeySuffix: "[user={{ request.user.id }}]", // don't load data for other users + customKeySuffix: `[user=${requestUserId}]`, // don't load data for other users onRestore: function () { // restore publish confirmation state if (textResultsPublishConfirmation.bottomCard) { @@ -220,18 +226,18 @@ const formSaver = new AutoFormSaver(document.getElementById("student-vote-form") }, onSave: function () { const timeNow = new Date(); - localStorage.setItem(lastSavedStorageKey, timeNow.toString()); + localStorage.setItem(languageStorageKey, timeNow.toString()); }, }); +const languageCode = dataElement.dataset.language_code!; function updateLastSavedLabel() { const timeNow = new Date(); const lastSavedLabel = document.getElementById('last-saved')!; - const lastSavedStorageValue = localStorage.getItem(lastSavedStorageKey); + const lastSavedStorageValue = localStorage.getItem(languageStorageKey); if (lastSavedStorageValue !== null) { const lastSavedDate = new Date(lastSavedStorageValue); const delta = Math.round((timeNow.getTime() - lastSavedDate.getTime()) / 1000); - const languageCode = "{{ LANGUAGE_CODE }}"; const relativeTimeFormat = new Intl.RelativeTimeFormat(languageCode); let timeStamp; if (delta < 3) { @@ -275,6 +281,8 @@ const textAnswerWarnings = document.getElementById("text-answer-warnings") as HT initTextAnswerWarnings(document.querySelectorAll("#student-vote-form textarea"), JSON.parse(textAnswerWarnings.textContent) as string[][]); const form = document.getElementById('student-vote-form') as HTMLFormElement; +const successMagicString = dataElement.dataset.success_magic_string!; +const successRedirectUrl = dataElement.dataset.success_redirect_url!; const submitListener = (event: Event) => { event.preventDefault(); // don't use the default submission const submitButton = document.getElementById('vote-submit-btn') as HTMLButtonElement; @@ -291,9 +299,9 @@ const submitListener = (event: Event) => { assert(response.ok); return response.text(); }).then(response_text => { - if (response_text === "{{ success_magic_string }}") { + if (response_text === successMagicString) { formSaver.releaseData(); - window.location.replace("{{ success_redirect_url }}"); + window.location.replace(successRedirectUrl); } else { // resubmit without this handler to show the site with the form errors form.removeEventListener("submit", submitListener); diff --git a/evap/student/templates/student_vote.html b/evap/student/templates/student_vote.html index 0cea1159d7..39012f36cb 100644 --- a/evap/student/templates/student_vote.html +++ b/evap/student/templates/student_vote.html @@ -6,6 +6,14 @@ {% block title %}{{ evaluation.full_name }} - {% translate 'Evaluation' %} - {{ block.super }}{% endblock %} +
+ {% block breadcrumb_bar %} - +
{% for language_code, language_name in languages %} Date: Mon, 15 Dec 2025 21:02:32 +0100 Subject: [PATCH 03/10] Fix misc stuff --- evap/static/ts/src/student-vote.ts | 119 ++++++++++++++++------------- evap/static/ts/tsconfig.json | 2 +- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/evap/static/ts/src/student-vote.ts b/evap/static/ts/src/student-vote.ts index f13f1edaad..4db6887465 100644 --- a/evap/static/ts/src/student-vote.ts +++ b/evap/static/ts/src/student-vote.ts @@ -3,10 +3,7 @@ import { AutoFormSaver } from "./auto-form-saver.js"; import { CSRF_HEADERS } from "./csrf-utils.js"; import { initTextAnswerWarnings } from "./text-answer-warnings.js"; import { assert } from "./utils.js"; - -// TODO: templates -// TODO: bootstrap -// TODO: for attribute on Button +import { Collapse } from "bootstrap"; function isInvisible(el: Element): boolean { if (getComputedStyle(el).display === "none") { @@ -137,11 +134,11 @@ studentForm.addEventListener("keydown", (e: KeyboardEvent) => { // Just giving back control to the browser here doesn't work, because // it would navigate backwards through the controls of the current row. disableFocusHandler = true; - selectables[0].focus({preventScroll: true}); + selectables[0].focus({ preventScroll: true }); return; } else if (nextRowIndex === rows.length) { // User wants to tab out behind the form area - selectables[selectables.length - 1].focus({preventScroll: true}); + selectables[selectables.length - 1].focus({ preventScroll: true }); return; } } while (isInvisible(rows[nextRowIndex]) || !hasTabbingTarget(rows[nextRowIndex])); @@ -168,7 +165,7 @@ function findCorrectInputInRow(row: HTMLElement) { } function fancyFocus(element: HTMLElement) { - element.focus({preventScroll: true}); + element.focus({ preventScroll: true }); element.scrollIntoView({ behavior: "smooth", block: "center", @@ -185,7 +182,6 @@ function scrollToFirstChoiceError() { } } - const dataElement = document.querySelector(".dataElement")!; const evaluationId = dataElement.dataset.evaluation_id!; const requestUserId = dataElement.dataset.request_user_id; @@ -233,7 +229,7 @@ const formSaver = new AutoFormSaver(document.getElementById("student-vote-form") const languageCode = dataElement.dataset.language_code!; function updateLastSavedLabel() { const timeNow = new Date(); - const lastSavedLabel = document.getElementById('last-saved')!; + const lastSavedLabel = document.getElementById("last-saved")!; const lastSavedStorageValue = localStorage.getItem(languageStorageKey); if (lastSavedStorageValue !== null) { const lastSavedDate = new Date(lastSavedStorageValue); @@ -249,15 +245,21 @@ function updateLastSavedLabel() { } else if (delta < 60) { timeStamp = "{% translate 'less than 1 minute ago' %}"; } else if (delta < 60 * 30) { - timeStamp = relativeTimeFormat.format(-Math.round(delta / 60), 'minutes'); + timeStamp = relativeTimeFormat.format(-Math.round(delta / 60), "minutes"); } else if (delta < 60 * 60 * 12) { - timeStamp = padWithLeadingZeros(lastSavedDate.getHours()) + ":" + padWithLeadingZeros(lastSavedDate.getMinutes()); + timeStamp = + padWithLeadingZeros(lastSavedDate.getHours()) + ":" + padWithLeadingZeros(lastSavedDate.getMinutes()); } else { - timeStamp = lastSavedDate.getFullYear().toString() + "-" - + padWithLeadingZeros(lastSavedDate.getMonth() + 1) + "-" - + padWithLeadingZeros(lastSavedDate.getDate()) + " " - + padWithLeadingZeros(lastSavedDate.getHours()) + ":" - + padWithLeadingZeros(lastSavedDate.getMinutes()); + timeStamp = + lastSavedDate.getFullYear().toString() + + "-" + + padWithLeadingZeros(lastSavedDate.getMonth() + 1) + + "-" + + padWithLeadingZeros(lastSavedDate.getDate()) + + " " + + padWithLeadingZeros(lastSavedDate.getHours()) + + ":" + + padWithLeadingZeros(lastSavedDate.getMinutes()); } lastSavedLabel.innerText = "{% translate 'Last saved locally' %}: " + timeStamp; } else { @@ -278,14 +280,17 @@ updateLastSavedLabel(); setInterval(updateLastSavedLabel, 1000); const textAnswerWarnings = document.getElementById("text-answer-warnings") as HTMLTextAreaElement; -initTextAnswerWarnings(document.querySelectorAll("#student-vote-form textarea"), JSON.parse(textAnswerWarnings.textContent) as string[][]); +initTextAnswerWarnings( + document.querySelectorAll("#student-vote-form textarea"), + JSON.parse(textAnswerWarnings.textContent) as string[][], +); -const form = document.getElementById('student-vote-form') as HTMLFormElement; +const form = document.getElementById("student-vote-form") as HTMLFormElement; const successMagicString = dataElement.dataset.success_magic_string!; const successRedirectUrl = dataElement.dataset.success_redirect_url!; const submitListener = (event: Event) => { event.preventDefault(); // don't use the default submission - const submitButton = document.getElementById('vote-submit-btn') as HTMLButtonElement; + const submitButton = document.getElementById("vote-submit-btn") as HTMLButtonElement; const originalText = submitButton.innerText; submitButton.innerText = "{% translate 'Submitting...' %}"; @@ -295,31 +300,37 @@ const submitListener = (event: Event) => { body: new FormData(form), headers: CSRF_HEADERS, method: form.method, - }).then(response => { - assert(response.ok); - return response.text(); - }).then(response_text => { - if (response_text === successMagicString) { - formSaver.releaseData(); - window.location.replace(successRedirectUrl); - } else { - // resubmit without this handler to show the site with the form errors - form.removeEventListener("submit", submitListener); - form.requestSubmit(); - } - }).catch((_: unknown) => { - // show a warning if the post isn't successful - document.getElementById('submit-error-warning')!.classList.remove("d-none"); - submitButton.innerText = originalText; - submitButton.disabled = false; - }); + }) + .then(response => { + assert(response.ok); + return response.text(); + }) + .then(response_text => { + if (response_text === successMagicString) { + formSaver.releaseData(); + window.location.replace(successRedirectUrl); + } else { + // resubmit without this handler to show the site with the form errors + form.removeEventListener("submit", submitListener); + form.requestSubmit(); + } + }) + .catch((_: unknown) => { + // show a warning if the post isn't successful + document.getElementById("submit-error-warning")!.classList.remove("d-none"); + submitButton.innerText = originalText; + submitButton.disabled = false; + }); }; form.addEventListener("submit", submitListener); -function clearChoiceError(voteButton: HTMLInputElement) { - voteButton.closest(".row")!.querySelectorAll(".choice-error").forEach(highlightedElement => { - highlightedElement.classList.remove("choice-error"); - }); +function clearChoiceError(voteButton: HTMLElement) { + voteButton + .closest(".row")! + .querySelectorAll(".choice-error") + .forEach(highlightedElement => { + highlightedElement.classList.remove("choice-error"); + }); } document.querySelectorAll("[data-mark-no-answers-for]").forEach(button => { @@ -336,7 +347,7 @@ document.querySelectorAll("[data-mark-no-answers-for]").forEa formSaver.saveAllData(); // hide questionnaire for contributor - const voteAreaCollapse = bootstrap.Collapse.getOrCreateInstance(voteArea); + const voteAreaCollapse = Collapse.getOrCreateInstance(voteArea); voteAreaCollapse.hide(); collapseToggle.classList.add("tab-selectable"); @@ -345,14 +356,13 @@ document.querySelectorAll("[data-mark-no-answers-for]").forEa button.disabled = true; }); - voteArea.querySelectorAll(".vote-inputs [type=radio]:not([value='6'])") - .forEach(radioInput => { - radioInput.addEventListener("click", () => { - collapseToggle.classList.remove("tab-selectable"); - button.classList.add("tab-selectable"); - button.disabled = false; - }); + voteArea.querySelectorAll(".vote-inputs [type=radio]:not([value='6'])").forEach(radioInput => { + radioInput.addEventListener("click", () => { + collapseToggle.classList.remove("tab-selectable"); + button.classList.add("tab-selectable"); + button.disabled = false; }); + }); collapseToggle.addEventListener("click", () => { if (button.classList.contains("tab-selectable")) { @@ -362,15 +372,18 @@ document.querySelectorAll("[data-mark-no-answers-for]").forEa }); // remove error highlighting when an answer was selected -document.querySelectorAll(".vote-btn.choice-error").forEach(voteButton => { +document.querySelectorAll(".vote-btn.choice-error").forEach(voteButton => { voteButton.addEventListener("click", () => clearChoiceError(voteButton)); - const actualInput = document.getElementById(voteButton.attributes.for.value)!; + console.log(voteButton.attributes); + const actualInput = document.getElementById(voteButton.htmlFor)!; actualInput.addEventListener("click", () => clearChoiceError(voteButton)); }); document.querySelectorAll(".btn-textanswer").forEach(textanswerButton => { const textfieldClass = textanswerButton.dataset.bsTarget!; - const textfield = textanswerButton.closest(".row")!.querySelector(textfieldClass + " textarea")!; + const textfield = textanswerButton + .closest(".row")! + .querySelector(textfieldClass + " textarea")!; textanswerButton.addEventListener("click", () => { // focus textarea when opening the collapsed area const isOpening = textanswerButton.classList.contains("collapsed"); @@ -406,4 +419,4 @@ if (textResultsPublishConfirmation.bottomCard) { textResultsPublishConfirmation.top.checked = textResultsPublishConfirmation.bottom.checked; formSaver.saveAllData(); }); -} \ No newline at end of file +} diff --git a/evap/static/ts/tsconfig.json b/evap/static/ts/tsconfig.json index c765d806e0..e47ea2e495 100644 --- a/evap/static/ts/tsconfig.json +++ b/evap/static/ts/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": ".", - "target": "es2019", + "target": "es2020", "strict": true, "esModuleInterop": true, "moduleResolution": "node" From bf94f07d4dac2990715b56f3ae728837dd1cda08 Mon Sep 17 00:00:00 2001 From: styrix560 Date: Mon, 15 Dec 2025 21:04:05 +0100 Subject: [PATCH 04/10] Remove unecessary whitespace --- evap/student/templates/student_vote.html | 1 - 1 file changed, 1 deletion(-) diff --git a/evap/student/templates/student_vote.html b/evap/student/templates/student_vote.html index 39012f36cb..53ddc6b2bf 100644 --- a/evap/student/templates/student_vote.html +++ b/evap/student/templates/student_vote.html @@ -68,7 +68,6 @@

{% translate "Questionnaire language" %}

- + - + - +
{% for language_code, language_name in languages %} Date: Mon, 19 Jan 2026 18:31:35 +0100 Subject: [PATCH 08/10] wip --- evap/static/ts/src/student-vote.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/evap/static/ts/src/student-vote.ts b/evap/static/ts/src/student-vote.ts index c1ea416c36..c2a2e7e989 100644 --- a/evap/static/ts/src/student-vote.ts +++ b/evap/static/ts/src/student-vote.ts @@ -14,6 +14,7 @@ function isInvisible(el: Element): boolean { function hasTabbingTarget(element: HTMLElement): boolean { return element.querySelector(".tab-selectable") !== null; } +console.log("here"); function selectByNumberKey(row: HTMLElement, num: number) { let index = 2 * num - 1; @@ -332,7 +333,9 @@ function clearChoiceError(voteButton: HTMLElement) { }); } +console.log("here"); document.querySelectorAll("[data-mark-no-answers-for]").forEach(button => { + console.log("here"); const contributorId = button.dataset.markNoAnswersFor!; const voteArea = document.getElementById(`vote-area-${contributorId}`)!; const collapseToggle = voteArea.closest(".collapsible")!.querySelector(".collapse-toggle")!; From ce545d526b3f9bd602fbee209b6f2359dfbc5256 Mon Sep 17 00:00:00 2001 From: styrix560 Date: Mon, 19 Jan 2026 19:52:36 +0100 Subject: [PATCH 09/10] wip --- evap/static/ts/src/student-vote.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/evap/static/ts/src/student-vote.ts b/evap/static/ts/src/student-vote.ts index c2a2e7e989..62f1358527 100644 --- a/evap/static/ts/src/student-vote.ts +++ b/evap/static/ts/src/student-vote.ts @@ -1,8 +1,8 @@ +// declare const bootstrap: typeof import("bootstrap"); import { selectOrError, assert } from "./utils.js"; import { AutoFormSaver } from "./auto-form-saver.js"; import { CSRF_HEADERS } from "./csrf-utils.js"; import { initTextAnswerWarnings } from "./text-answer-warnings.js"; -import { Collapse } from "bootstrap"; function isInvisible(el: Element): boolean { if (getComputedStyle(el).display === "none") { @@ -349,7 +349,8 @@ document.querySelectorAll("[data-mark-no-answers-for]").forEa formSaver.saveAllData(); // hide questionnaire for contributor - const voteAreaCollapse = Collapse.getOrCreateInstance(voteArea); + // @ts-ignore + const voteAreaCollapse = bootstrap.Collapse.getOrCreateInstance(voteArea); voteAreaCollapse.hide(); collapseToggle.classList.add("tab-selectable"); From 57cde42083ae55ba511a316480ba0f001cfeeb9c Mon Sep 17 00:00:00 2001 From: styrix560 Date: Mon, 19 Jan 2026 19:52:43 +0100 Subject: [PATCH 10/10] das hier ist der wichtige --- evap/student/templates/student_vote.html | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/evap/student/templates/student_vote.html b/evap/student/templates/student_vote.html index 39012f36cb..320db72ec3 100644 --- a/evap/student/templates/student_vote.html +++ b/evap/student/templates/student_vote.html @@ -6,14 +6,6 @@ {% block title %}{{ evaluation.full_name }} - {% translate 'Evaluation' %} - {{ block.super }}{% endblock %} -
- {% block breadcrumb_bar %}