From 63d1ed8bd2957b9c1e95ee12da54867811cc798f Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Wed, 16 Apr 2025 13:02:13 +0200 Subject: [PATCH] feat(frontend): Vue can now interact with the chosen answers for questions. --- .../processing/gift/gift-processor.ts | 2 +- .../learning-paths/LearningObjectView.vue | 46 +++++++++++++++++++ .../gift-adapters/essay-question-adapter.ts | 13 ++++++ .../gift-adapters/gift-adapter.d.ts | 5 ++ .../gift-adapters/gift-adapters.ts | 8 ++++ .../multiple-choice-question-adapter.ts | 26 +++++++++++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 frontend/src/views/learning-paths/gift-adapters/essay-question-adapter.ts create mode 100644 frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts create mode 100644 frontend/src/views/learning-paths/gift-adapters/gift-adapters.ts create mode 100644 frontend/src/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts diff --git a/backend/src/services/learning-objects/processing/gift/gift-processor.ts b/backend/src/services/learning-objects/processing/gift/gift-processor.ts index 8d548f56..d6fe5adb 100644 --- a/backend/src/services/learning-objects/processing/gift/gift-processor.ts +++ b/backend/src/services/learning-objects/processing/gift/gift-processor.ts @@ -38,7 +38,7 @@ class GiftProcessor extends StringProcessor { let html = "
\n"; let i = 1; for (const question of quizQuestions) { - html += `
\n`; + html += `
\n`; html += ' ' + this.renderQuestion(question, i).replaceAll(/\n(.+)/g, '\n $1'); // Replace for indentation. html += `
\n`; i++; diff --git a/frontend/src/views/learning-paths/LearningObjectView.vue b/frontend/src/views/learning-paths/LearningObjectView.vue index 25fd5672..a80f2625 100644 --- a/frontend/src/views/learning-paths/LearningObjectView.vue +++ b/frontend/src/views/learning-paths/LearningObjectView.vue @@ -3,6 +3,8 @@ import type { UseQueryReturnType } from "@tanstack/vue-query"; import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts"; import UsingQueryResult from "@/components/UsingQueryResult.vue"; + import {nextTick, onMounted, reactive, watch} from "vue"; + import {getGiftAdapterForType} from "@/views/learning-paths/gift-adapters/gift-adapters.ts"; const props = defineProps<{ hruid: string; language: Language; version: number }>(); @@ -11,6 +13,49 @@ () => props.language, () => props.version, ); + + const currentAnswer = reactive([]); + + function forEachQuestion( + doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void + ) { + const questions = document.querySelectorAll(".gift-question"); + questions.forEach(question => { + const name = question.id.match(/gift-q(\d+)/)?.[1] + const questionType = question.classList.values() + .find(it => it.startsWith("gift-question-type")) + .match(/gift-question-type-([^ ]*)/)?.[1]; + + if (!name || isNaN(parseInt(name)) || !questionType) return; + + const index = parseInt(name) - 1; + + doAction(index, name, questionType, question); + }); + } + + function attachQuestionListeners() { + forEachQuestion((index, name, type, element) => { + getGiftAdapterForType(type)?.installListener( + element, (newAnswer) => currentAnswer[index] = newAnswer + ); + }); + } + + function setAnswers(answers: (object | string | number)[]) { + forEachQuestion((index, name, type, element) => { + getGiftAdapterForType(type)?.setAnswer(element, answers[index]); + }); + currentAnswer.fill(answers); + } + + onMounted(() => nextTick(() => attachQuestionListeners())); + + watch(learningObjectHtmlQueryResult.data, async () => { + await nextTick(); + attachQuestionListeners(); + setAnswers([1]); + }); diff --git a/frontend/src/views/learning-paths/gift-adapters/essay-question-adapter.ts b/frontend/src/views/learning-paths/gift-adapters/essay-question-adapter.ts new file mode 100644 index 00000000..eb49027c --- /dev/null +++ b/frontend/src/views/learning-paths/gift-adapters/essay-question-adapter.ts @@ -0,0 +1,13 @@ +export const essayQuestionAdapter: GiftAdapter = { + questionType: "Essay", + + installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void { + const textArea = questionElement.querySelector('textarea')!; + textArea.addEventListener('input', () => answerUpdateCallback(textArea.value)); + }, + + setAnswer(questionElement: Element, answer: string | number | object): void { + const textArea = questionElement.querySelector('textarea')!; + textArea.value = String(answer); + } +} diff --git a/frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts b/frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts new file mode 100644 index 00000000..b93234d9 --- /dev/null +++ b/frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts @@ -0,0 +1,5 @@ +interface GiftAdapter { + questionType: string; + installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void; + setAnswer(questionElement: Element, answer: string | number | object): void; +} diff --git a/frontend/src/views/learning-paths/gift-adapters/gift-adapters.ts b/frontend/src/views/learning-paths/gift-adapters/gift-adapters.ts new file mode 100644 index 00000000..d4a49ef2 --- /dev/null +++ b/frontend/src/views/learning-paths/gift-adapters/gift-adapters.ts @@ -0,0 +1,8 @@ +import {multipleChoiceQuestionAdapter} from "@/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts"; +import {essayQuestionAdapter} from "@/views/learning-paths/gift-adapters/essay-question-adapter.ts"; + +export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter]; + +export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined { + return giftAdapters.find(it => it.questionType === questionType); +} diff --git a/frontend/src/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts b/frontend/src/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts new file mode 100644 index 00000000..3898cb97 --- /dev/null +++ b/frontend/src/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts @@ -0,0 +1,26 @@ +export const multipleChoiceQuestionAdapter: GiftAdapter = { + questionType: "MC", + + installListener(questionElement: Element, answerUpdateCallback: (newAnswer: string | number | object) => void): void { + questionElement.querySelectorAll('input[type=radio]').forEach(element => { + const input = element as HTMLInputElement; + + input.addEventListener('change', () => { + answerUpdateCallback(parseInt(input.value)); + }); + // Optional: initialize value if already selected + if (input.checked) { + answerUpdateCallback(parseInt(input.value)); + } + }); + }, + + setAnswer(questionElement: Element, answer: string | number | object): void { + questionElement.querySelectorAll('input[type=radio]').forEach(element => { + const input = element as HTMLInputElement; + console.log(`input: ${input.value}, answer: ${answer}`); + input.checked = String(answer) === String(input.value); + console.log(input.checked); + }); + } +}