From 1e84109d1b800500012cc0be42022729af313713 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 6 Apr 2026 13:35:35 +0200 Subject: [PATCH 01/35] move ChoiceAnswer model to folder --- src/models/documents/{ChoiceAnswer.ts => ChoiceAnswer/index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/models/documents/{ChoiceAnswer.ts => ChoiceAnswer/index.ts} (100%) diff --git a/src/models/documents/ChoiceAnswer.ts b/src/models/documents/ChoiceAnswer/index.ts similarity index 100% rename from src/models/documents/ChoiceAnswer.ts rename to src/models/documents/ChoiceAnswer/index.ts From 519535ab1c249d62396b0318d7a615c1c9db0a1c Mon Sep 17 00:00:00 2001 From: bh0fer Date: Wed, 8 Apr 2026 10:28:37 +0200 Subject: [PATCH 02/35] add some basic tests --- .../documents/ChoiceAnswer/Quiz/index.tsx | 6 +- .../remark-transform-choice-answer/plugin.ts | 19 ++- .../tests/artifacts/.gitkeep | 0 .../tests/plugin.test.ts | 117 ++++++++++++++++++ 4 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 src/plugins/remark-transform-choice-answer/tests/artifacts/.gitkeep create mode 100644 src/plugins/remark-transform-choice-answer/tests/plugin.test.ts diff --git a/src/components/documents/ChoiceAnswer/Quiz/index.tsx b/src/components/documents/ChoiceAnswer/Quiz/index.tsx index f0af7df3f..d6d928d88 100644 --- a/src/components/documents/ChoiceAnswer/Quiz/index.tsx +++ b/src/components/documents/ChoiceAnswer/Quiz/index.tsx @@ -20,7 +20,7 @@ interface Props { randomizeQuestions?: boolean; scoring?: ScoringFunction; minPoints?: number; - numQuestions: number; + questionCount: number; children?: React.ReactNode[]; } @@ -55,9 +55,9 @@ const Quiz = observer((props: Props) => { React.useEffect(() => { if (props.randomizeQuestions && !doc?.data.questionOrder) { - doc?.updateQuestionOrder(createRandomOrderMap(props.numQuestions)); + doc?.updateQuestionOrder(createRandomOrderMap(props.questionCount)); } - }, [props.randomizeQuestions, doc, props.numQuestions]); + }, [props.randomizeQuestions, doc, props.questionCount]); if (!doc) { return ; diff --git a/src/plugins/remark-transform-choice-answer/plugin.ts b/src/plugins/remark-transform-choice-answer/plugin.ts index cff572ec2..71a2d3ff9 100644 --- a/src/plugins/remark-transform-choice-answer/plugin.ts +++ b/src/plugins/remark-transform-choice-answer/plugin.ts @@ -1,8 +1,10 @@ import { visit } from 'unist-util-visit'; -import type { Plugin } from 'unified'; +import type { Plugin, Transformer } from 'unified'; import type { Root, BlockContent, DefinitionContent } from 'mdast'; import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx'; -import { toMdxJsxExpressionAttribute } from '../helpers'; +import { toJsxAttribute, toMdxJsxExpressionAttribute } from '../helpers'; +import path from 'path'; +import { promises as fs } from 'fs'; enum ChoiceComponentTypes { ChoiceAnswer = 'ChoiceAnswer', @@ -127,6 +129,7 @@ const transformQuestions = (questionNodes: MdxJsxFlowElement[]) => { const transformQuiz = (quizNode: MdxJsxFlowElement) => { const questions = [] as MdxJsxFlowElement[]; + visit(quizNode, 'mdxJsxFlowElement', (childNode) => { if (Object.values(ChoiceComponentTypes).includes(childNode.name as ChoiceComponentTypes)) { questions.push(childNode); @@ -134,17 +137,11 @@ const transformQuiz = (quizNode: MdxJsxFlowElement) => { }); transformQuestions(questions); - quizNode.attributes.push( - toMdxJsxExpressionAttribute('numQuestions', true, { - type: 'Literal', - value: questions.length, - raw: `${questions.length}` - }) - ); + quizNode.attributes.push(toJsxAttribute('questionCount', questions.length)); }; -const plugin: Plugin<[], Root> = function choiceAnswerWrapPlugin() { - return (tree) => { +const plugin: Plugin<[], Root> = function choiceAnswerWrapPlugin(this, options = []): Transformer { + return async (tree, vfile) => { visit(tree, 'mdxJsxFlowElement', (node) => { if (node.name === QUIZ_NODE_NAME) { // Enumerate and transform questions inside the quiz. diff --git a/src/plugins/remark-transform-choice-answer/tests/artifacts/.gitkeep b/src/plugins/remark-transform-choice-answer/tests/artifacts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts b/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts new file mode 100644 index 000000000..ab4352b74 --- /dev/null +++ b/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts @@ -0,0 +1,117 @@ +import { remark } from 'remark'; +import remarkMdx from 'remark-mdx'; +import { VFile } from 'vfile'; +import { fileURLToPath } from 'url'; +import { afterEach, describe, expect, it } from 'vitest'; +import path from 'path'; +import { promises as fs } from 'fs'; + +const __filename = fileURLToPath(import.meta.url); + +const alignLeft = (content: string) => { + return content + .split('\n') + .map((line) => line.trimStart()) + .join('\n'); +}; + +const process = async (content: string) => { + const { default: plugin } = await import('../plugin'); + const tmpFile = path.resolve(path.dirname(__filename), 'artifacts', `test-${Date.now()}.mdx`); + await fs.writeFile(tmpFile, alignLeft(content)); + const file = new VFile({ value: alignLeft(content), history: [tmpFile] }); + const result = await remark().use(remarkMdx).use(plugin).process(file); + + return result.value; +}; + +afterEach(() => { + // clear ./artifacts folder content + const artifactsDir = path.resolve(path.dirname(__filename), 'artifacts'); + fs.readdir(artifactsDir) + .then((files) => { + const unlinkPromises = files.map((file) => + file !== '.gitkeep' ? fs.unlink(path.join(artifactsDir, file)) : Promise.resolve() + ); + return Promise.all(unlinkPromises); + }) + .catch((err) => { + console.warn('Could not clear artifacts directory', err); + }); +}); + +describe('#quiz', () => { + it("does nothing if there's no quiz", async () => { + const input = `# Heading + + Some content + `; + const result = await process(input); + expect(result).toBe(alignLeft(input)); + }); + it('handles empty quiz', async () => { + const input = `# Heading + + + + `; + const result = await process(input); + expect(result).toMatchInlineSnapshot(` + "# Heading + + + " + `); + }); + it('handles quiz with questions', async () => { + const input = `# Heading + + + + > In welchem Jahr war 2024? + + 1. 1965 + 2. 1983 + 3. 1991 + 4. 2000 + 5. 2024 + + + `; + const result = await process(input); + expect(result).toMatchInlineSnapshot(` + "# Heading + + + + + > In welchem Jahr war 2024? + + + + + 1965 + + + + 1983 + + + + 1991 + + + + 2000 + + + + 2024 + + + + + " + `); + }); +}); From 7ee06649a313572bc2be782f4f3e8b3be1092036 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Tue, 21 Apr 2026 17:12:24 +0200 Subject: [PATCH 03/35] rewrite remark-transformer --- .../remark-transform-choice-answer/plugin.ts | 115 +++++-- .../tests/plugin.test.ts | 303 +++++++++++++++++- 2 files changed, 382 insertions(+), 36 deletions(-) diff --git a/src/plugins/remark-transform-choice-answer/plugin.ts b/src/plugins/remark-transform-choice-answer/plugin.ts index 71a2d3ff9..4e2352819 100644 --- a/src/plugins/remark-transform-choice-answer/plugin.ts +++ b/src/plugins/remark-transform-choice-answer/plugin.ts @@ -1,7 +1,7 @@ import { visit } from 'unist-util-visit'; import type { Plugin, Transformer } from 'unified'; import type { Root, BlockContent, DefinitionContent } from 'mdast'; -import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx'; +import type { MdxJsxAttribute, MdxJsxExpressionAttribute, MdxJsxFlowElement } from 'mdast-util-mdx'; import { toJsxAttribute, toMdxJsxExpressionAttribute } from '../helpers'; import path from 'path'; import { promises as fs } from 'fs'; @@ -50,7 +50,7 @@ const createWrappedOption = ( return { wrappedOptions: createWrapper(OPTIONS_WRAPPER_NAME, options), numOptions: options.length }; }; -const transformQuestion = (questionNode: MdxJsxFlowElement) => { +const transformQuestion = (questionNode: MdxJsxFlowElement, inQuiz?: boolean) => { const listIndex = questionNode.children.findIndex((child) => child.type === 'list'); if (listIndex === -1) { @@ -92,13 +92,10 @@ const transformQuestion = (questionNode: MdxJsxFlowElement) => { listChild.children as { type: string; children: FlowChildren }[] ); wrappedChildren.push(wrappedOptions); - questionNode.attributes.push( - toMdxJsxExpressionAttribute('numOptions', true, { - type: 'Literal', - value: numOptions, - raw: `${numOptions}` - }) - ); + questionNode.attributes.push(toJsxAttribute('optionsCount', numOptions)); + if (inQuiz) { + questionNode.attributes.push(toJsxAttribute('inQuiz', true)); + } if (afterChildren.length > 0) { wrappedChildren.push(createWrapper(AFTER_WRAPPER_NAME, afterChildren)); @@ -107,37 +104,91 @@ const transformQuestion = (questionNode: MdxJsxFlowElement) => { questionNode.children = wrappedChildren; }; -const transformQuestions = (questionNodes: MdxJsxFlowElement[]) => { - questionNodes.forEach((questionNode, index: number) => { - transformQuestion(questionNode); - questionNode.attributes.push( - toMdxJsxExpressionAttribute('questionIndex', true, { - type: 'Literal', - value: index, - raw: `${index}` - }) - ); - questionNode.attributes.push( - toMdxJsxExpressionAttribute('inQuiz', true, { - type: 'Literal', - value: true, - raw: 'true' - }) - ); - }); +const isLiteralExpression = ( + attr?: MdxJsxAttribute +): attr is MdxJsxAttribute & { + value: { type: 'mdxJsxAttributeValueExpression'; value: string; data: { estree: any } }; +} => { + if (!attr) { + return false; + } + return ( + attr.value !== null && + typeof attr.value === 'object' && + attr.value.type === 'mdxJsxAttributeValueExpression' && + attr.value.data?.estree?.body.length === 1 && + attr.value.data.estree.body[0].type === 'ExpressionStatement' && + attr.value.data.estree.body[0].expression.type === 'Literal' + ); +}; + +const qidGenerator = () => { + const qidMap = new Map(); + let counter = 1; + const generator = (qid?: MdxJsxAttribute | MdxJsxExpressionAttribute) => { + if (qid && qid.type === 'mdxJsxExpressionAttribute') { + throw new Error('Expression attributes are not supported for qid generation'); + } + const literalExpression = isLiteralExpression(qid) ? qid : null; + if (qid && qid.value && typeof qid.value !== 'string' && !literalExpression) { + qidMap.set(qid.value.value, qid); + return qid; + } + + let id = qid + ? literalExpression + ? `${literalExpression.value.data.estree.body[0].expression.value}` + : (qid.value as string) + : `q${counter++}`; + + let localCounter = 1; + while (qidMap.has(id)) { + if (qid) { + id = `${id}.${localCounter++}`; + } else { + id = `q${counter++}`; + } + } + const jsxAttr = toJsxAttribute('qid', id); + qidMap.set(id, jsxAttr); + return jsxAttr; + }; + return { generator, qidSet: qidMap }; }; const transformQuiz = (quizNode: MdxJsxFlowElement) => { - const questions = [] as MdxJsxFlowElement[]; + const { generator: getQid, qidSet } = qidGenerator(); visit(quizNode, 'mdxJsxFlowElement', (childNode) => { + // TODO: use a set for the check if (Object.values(ChoiceComponentTypes).includes(childNode.name as ChoiceComponentTypes)) { - questions.push(childNode); + transformQuestion(childNode, true); + const qidIdx = childNode.attributes.findIndex((attr) => (attr as any).name === 'qid'); + const qidAttr = getQid(childNode.attributes[qidIdx]); + if (qidIdx === -1) { + childNode.attributes.push(qidAttr); + } else if (qidAttr !== childNode.attributes[qidIdx]) { + childNode.attributes[qidIdx] = qidAttr; + } } }); - - transformQuestions(questions); - quizNode.attributes.push(toJsxAttribute('questionCount', questions.length)); + const qids = toMdxJsxExpressionAttribute('questionIds', [...qidSet.keys()], { + type: 'ArrayExpression', + elements: [...qidSet.values()].map((val) => { + if (typeof val.value === 'string') { + return { + type: 'Literal', + value: val.value, + raw: `'${val.value}'` + }; + } + if (!val.value) { + throw new Error('Unexpected non-string qid value'); + } + return (val.value.data?.estree?.body[0] as { expression: any })?.expression; + }) + }); + quizNode.attributes.push(qids); }; const plugin: Plugin<[], Root> = function choiceAnswerWrapPlugin(this, options = []): Transformer { diff --git a/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts b/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts index ab4352b74..a3ef3443b 100644 --- a/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts +++ b/src/plugins/remark-transform-choice-answer/tests/plugin.test.ts @@ -40,6 +40,8 @@ afterEach(() => { }); }); +// TODO: add test for nested quizzes - `transformQuiz` will likely fail since no depth is respected. + describe('#quiz', () => { it("does nothing if there's no quiz", async () => { const input = `# Heading @@ -59,14 +61,14 @@ describe('#quiz', () => { expect(result).toMatchInlineSnapshot(` "# Heading - + " `); }); it('handles quiz with questions', async () => { const input = `# Heading - + > In welchem Jahr war 2024? @@ -82,8 +84,8 @@ describe('#quiz', () => { expect(result).toMatchInlineSnapshot(` "# Heading - - + + > In welchem Jahr war 2024? @@ -114,4 +116,297 @@ describe('#quiz', () => { " `); }); + it('handles quiz with multiple questions', async () => { + const input = `# Heading + + + + > In welchem Jahr war 2024? + + 1. 1965 + 2. 1983 + 3. 1991 + 4. 2000 + 5. 2024 + + + + + > Wählen Sie eine korrekte Aussage aus. + + 1. Wenn Daten in der Cloud (z.B. OneDrive) gespeichert werden, ist kein Backup mehr nötig. + 2. Cloud-Dienste wie OneDrive erlauben es uns, Dateien mit anderen Personen zu teilen. + 3. Cloud-Dienste machen es einfacher, Dateien zwischen verschiedenen Geräten zu synchronisieren. + 4. Das Abspeichern sensibler Daten auf Cloud-Diensten ist immer unproblematisch, da diese ja sicher sind. + 5. Alle der oben genannten Aussagen sind korrekt. + + + + > HTML ist eine Programmiersprache. + + + + > Welche der folgenden Protokolle werden für die Übertragung von E-Mails verwendet? + + 1. SMTP + 2. FTP + 3. IMAP + 4. HTTP + + + + > Wann ist der Sinn des Lebens? + + 1. Immer im März + 2. 42 + 3. Das Bundeshaus + 4. Nein + 5. Ja, aber nur manchmal + + + `; + const result = await process(input); + expect(result).toMatchInlineSnapshot(` + "# Heading + + + + + > In welchem Jahr war 2024? + + + + + 1965 + + + + 1983 + + + + 1991 + + + + 2000 + + + + 2024 + + + + + + + > Wählen Sie eine korrekte Aussage aus. + + + + + Wenn Daten in der Cloud (z.B. OneDrive) gespeichert werden, ist kein Backup mehr nötig. + + + + Cloud-Dienste wie OneDrive erlauben es uns, Dateien mit anderen Personen zu teilen. + + + + Cloud-Dienste machen es einfacher, Dateien zwischen verschiedenen Geräten zu synchronisieren. + + + + Das Abspeichern sensibler Daten auf Cloud-Diensten ist immer unproblematisch, da diese ja sicher sind. + + + + Alle der oben genannten Aussagen sind korrekt. + + + + + + + + > HTML ist eine Programmiersprache. + + + + + + + > Welche der folgenden Protokolle werden für die Übertragung von E-Mails verwendet? + + + + + SMTP + + + + FTP + + + + IMAP + + + + HTTP + + + + + + + > Wann ist der Sinn des Lebens? + + + + + Immer im März + + + + 42 + + + + Das Bundeshaus + + + + Nein + + + + Ja, aber nur manchmal + + + + + " + `); + }); + + it('handles quiz with explicit set question ids and enumerates the remaining questions', async () => { + const input = `# Heading + + + + + + + + + + + + `; + const result = await process(input); + expect(result).toMatchInlineSnapshot(` + "# Heading + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " + `); + }); +}); + +describe('#standalone question', () => { + it('transforms standalone question', async () => { + const input = `# Heading + + > In welchem Jahr war 2024? + + 1. 1965 + 2. 1983 + 3. 1991 + 4. 2000 + 5. 2024 + + `; + const result = await process(input); + expect(result).toMatchInlineSnapshot(` + "# Heading + + + + > In welchem Jahr war 2024? + + + + + 1965 + + + + 1983 + + + + 1991 + + + + 2000 + + + + 2024 + + + + " + `); + }); }); From cae081dd7d799c28d3b361eac5efe93ffd656071 Mon Sep 17 00:00:00 2001 From: Balthasar Hofer Date: Tue, 21 Apr 2026 17:27:03 +0200 Subject: [PATCH 04/35] Add new interface Props to Quiz component --- src/components/documents/ChoiceAnswer/Quiz/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/documents/ChoiceAnswer/Quiz/index.tsx b/src/components/documents/ChoiceAnswer/Quiz/index.tsx index d6d928d88..91b46702e 100644 --- a/src/components/documents/ChoiceAnswer/Quiz/index.tsx +++ b/src/components/documents/ChoiceAnswer/Quiz/index.tsx @@ -12,6 +12,7 @@ import { ScoringFunction } from '../helpers/scoring'; import { QuizScore } from '../Feedback'; import useIsBrowser from '@docusaurus/useIsBrowser'; + interface Props { id: string; readonly?: boolean; From 221e97dfedd8eb9dfd9b0803cea7b0bad92941a4 Mon Sep 17 00:00:00 2001 From: bh0fer Date: Tue, 28 Apr 2026 17:28:28 +0200 Subject: [PATCH 05/35] Refactor ChoiceAnswer to Quiz/ChoiceAnswer structure and update related components Co-authored-by: Copilot --- src/api/document.ts | 32 +++++++++++++--- .../ChoiceAnswer/Component/index.tsx | 25 +++++++++---- .../documents/ChoiceAnswer/Controls/index.tsx | 2 +- .../documents/ChoiceAnswer/Feedback/index.tsx | 2 +- .../documents/ChoiceAnswer/Hints/index.tsx | 2 +- .../documents/ChoiceAnswer/Quiz/index.tsx | 6 +-- .../ChoiceAnswer/helpers/assessment.ts | 2 +- .../ChoiceAnswer/helpers/scoring.tsx | 2 +- .../index.ts => Quiz/ChoiceAnswer.ts} | 21 +---------- src/models/documents/Quiz/iAssessable.ts | 37 +++++++++++++++++++ src/stores/DocumentStore.ts | 2 +- 11 files changed, 92 insertions(+), 41 deletions(-) rename src/models/documents/{ChoiceAnswer/index.ts => Quiz/ChoiceAnswer.ts} (92%) create mode 100644 src/models/documents/Quiz/iAssessable.ts diff --git a/src/api/document.ts b/src/api/document.ts index baf6b84cb..cc89b96b5 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -23,7 +23,7 @@ import ChoiceAnswer, { ChoiceAnswerChoices, ChoiceAnswerOptionOrders, ChoiceAnswerQuestionOrder -} from '@tdev-models/documents/ChoiceAnswer'; +} from '@tdev-models/documents/Quiz/ChoiceAnswer'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -46,6 +46,15 @@ export interface StringData { text: string; } +export interface QuizData { + // nothing yet, just a wrapper for now +} + +export interface BooleanAnswerData { + value: boolean | null; + assessed: boolean; +} + export interface ChoiceAnswerData { choices: ChoiceAnswerChoices; optionOrders: ChoiceAnswerOptionOrders; @@ -122,6 +131,11 @@ export interface ViewStoreTypeMapping { export type ViewStoreType = keyof ViewStoreTypeMapping; export type ViewStore = ViewStoreTypeMapping[ViewStoreType]; +export interface AssessableDocumentMapping { + ['boolean_answer']: BooleanAnswerData; + ['choice_answer']: ChoiceAnswerData; +} + export interface ContainerTypeDataMapping { ['_container_placeholder_']: { name: string }; // placeholder to avoid empty interface error } @@ -131,12 +145,13 @@ export interface TaskableDocumentMapping { ['progress_state']: ProgressStateData; } -export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeDataMapping { +export interface TypeDataMapping + extends TaskableDocumentMapping, ContainerTypeDataMapping, AssessableDocumentMapping { ['code']: CodeData; // TODO: rename to `code_version`? ['script_version']: ScriptVersionData; ['string']: StringData; - ['choice_answer']: ChoiceAnswerData; + ['quiz']: QuizData; ['quill_v2']: QuillV2Data; ['solution']: SolutionData; ['dir']: DirData; @@ -149,6 +164,7 @@ export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeD } export type ContainerType = keyof ContainerTypeDataMapping; export type TaskableType = keyof TaskableDocumentMapping; +export type AssessableType = keyof AssessableDocumentMapping; type KeysWithCode = { [K in keyof T]: 'code' extends keyof T[K] ? K : never; @@ -160,6 +176,11 @@ export interface ContainerTypeModelMapping { ['_container_placeholder_']: iDocumentContainer; // placeholder to avoid empty interface error } +export interface AssessableTypeModelMapping { + ['boolean_answer']: ChoiceAnswer; // TODO: implement BooleanAnswer model and replace this + ['choice_answer']: ChoiceAnswer; +} + export interface TaskableTypeModelMapping { ['task_state']: TaskState; ['progress_state']: ProgressState; @@ -168,12 +189,12 @@ export interface TaskableTypeModelMapping { type EnsureAllTaskable }> = T; null as unknown as EnsureAllTaskable; -export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTypeModelMapping { +export interface TypeModelMapping + extends TaskableTypeModelMapping, ContainerTypeModelMapping, AssessableTypeModelMapping { ['code']: Code; // TODO: rename to `code_version`? ['script_version']: ScriptVersion; ['string']: String; - ['choice_answer']: ChoiceAnswer; ['quill_v2']: QuillV2; ['solution']: Solution; ['dir']: Directory; @@ -192,6 +213,7 @@ export interface TypeModelMapping extends TaskableTypeModelMapping, ContainerTyp export type ContainerModelType = ContainerTypeModelMapping[ContainerType]; export type TaskableModelType = TaskableTypeModelMapping[TaskableType]; +export type AssessableModelType = AssessableTypeModelMapping[AssessableType]; export type DocumentType = keyof TypeModelMapping; export type DocumentModelType = TypeModelMapping[DocumentType]; diff --git a/src/components/documents/ChoiceAnswer/Component/index.tsx b/src/components/documents/ChoiceAnswer/Component/index.tsx index 47929854c..10fc8df36 100644 --- a/src/components/documents/ChoiceAnswer/Component/index.tsx +++ b/src/components/documents/ChoiceAnswer/Component/index.tsx @@ -2,7 +2,7 @@ import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument'; import ChoiceAnswerDocument, { ChoiceAnswerCorrectness, ModelMeta -} from '@tdev-models/documents/ChoiceAnswer'; +} from '@tdev-models/documents/Quiz/ChoiceAnswer'; import { observer } from 'mobx-react-lite'; import React from 'react'; import clsx from 'clsx'; @@ -19,19 +19,28 @@ import { FeedbackBadge } from '../Feedback'; import { ScoringFunction } from '../helpers/scoring'; import { assess } from '../helpers/assessment'; -export interface ChoiceAnswerProps { - id: string; +interface SharedProps { title?: string; correct?: number[]; scoring?: ScoringFunction; questionIndex?: number; - inQuiz?: boolean; multiple?: boolean; randomizeOptions?: boolean; - numOptions: number; + optionsCount: number; readonly?: boolean; children: React.ReactNode; } +export interface StandaloneProps extends SharedProps { + inQuiz?: false; + id: string; +} + +export interface InQuizProps extends SharedProps { + inQuiz: true; + qid: string; +} + +export type ChoiceAnswerProps = StandaloneProps | InQuizProps; interface ThinWrapperProps { children: React.ReactNode; @@ -77,10 +86,10 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { if (randomizeOptions && !doc?.data.optionOrders?.[questionIndex]) { doc?.updateOptionOrders({ ...doc.data.optionOrders, - [questionIndex]: createRandomOrderMap(props.numOptions) + [questionIndex]: createRandomOrderMap(props.optionsCount) }); } - }, [randomizeOptions, doc, questionIndex, props.numOptions]); + }, [randomizeOptions, doc, questionIndex, props.optionsCount]); React.useEffect(() => { if (!doc) { @@ -99,7 +108,7 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { props.multiple ?? false, questionIndex, correctOptions, - props.numOptions, + props.optionsCount, scoringFunction ); doc.updateAssessment(questionIndex, assessment); diff --git a/src/components/documents/ChoiceAnswer/Controls/index.tsx b/src/components/documents/ChoiceAnswer/Controls/index.tsx index b9257c038..478907150 100644 --- a/src/components/documents/ChoiceAnswer/Controls/index.tsx +++ b/src/components/documents/ChoiceAnswer/Controls/index.tsx @@ -4,7 +4,7 @@ import Button from '@tdev-components/shared/Button'; import SyncStatus from '@tdev-components/SyncStatus'; import { observer } from 'mobx-react-lite'; import { mdiCheckboxMarkedCircleAutoOutline, mdiRestore } from '@mdi/js'; -import ChoiceAnswerDocument from '@tdev-models/documents/ChoiceAnswer'; +import ChoiceAnswerDocument from '@tdev-models/documents/Quiz/ChoiceAnswer'; import clsx from 'clsx'; import useIsMobileView from '@tdev-hooks/useIsMobileView'; diff --git a/src/components/documents/ChoiceAnswer/Feedback/index.tsx b/src/components/documents/ChoiceAnswer/Feedback/index.tsx index 7d88c5f86..62cf6c17f 100644 --- a/src/components/documents/ChoiceAnswer/Feedback/index.tsx +++ b/src/components/documents/ChoiceAnswer/Feedback/index.tsx @@ -1,6 +1,6 @@ import { mdiCheckCircleOutline, mdiCloseCircleOutline, mdiProgressCheck, mdiProgressQuestion } from '@mdi/js'; import Icon from '@mdi/react'; -import ChoiceAnswerDocument, { ChoiceAnswerCorrectness } from '@tdev-models/documents/ChoiceAnswer'; +import ChoiceAnswerDocument, { ChoiceAnswerCorrectness } from '@tdev-models/documents/Quiz/ChoiceAnswer'; import { observer } from 'mobx-react-lite'; import styles from './styles.module.scss'; import React from 'react'; diff --git a/src/components/documents/ChoiceAnswer/Hints/index.tsx b/src/components/documents/ChoiceAnswer/Hints/index.tsx index e044aba88..446d199b5 100644 --- a/src/components/documents/ChoiceAnswer/Hints/index.tsx +++ b/src/components/documents/ChoiceAnswer/Hints/index.tsx @@ -2,7 +2,7 @@ import { mdiCloseCircleOutline, mdiInformationOutline } from '@mdi/js'; import Icon from '@mdi/react'; import Button from '@tdev-components/shared/Button'; import Card from '@tdev-components/shared/Card'; -import ChoiceAnswerDocument from '@tdev-models/documents/ChoiceAnswer'; +import ChoiceAnswerDocument from '@tdev-models/documents/Quiz/ChoiceAnswer'; import clsx from 'clsx'; import { observer } from 'mobx-react-lite'; import React from 'react'; diff --git a/src/components/documents/ChoiceAnswer/Quiz/index.tsx b/src/components/documents/ChoiceAnswer/Quiz/index.tsx index 91b46702e..9f77d15e2 100644 --- a/src/components/documents/ChoiceAnswer/Quiz/index.tsx +++ b/src/components/documents/ChoiceAnswer/Quiz/index.tsx @@ -1,8 +1,8 @@ import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument'; -import { ModelMeta } from '@tdev-models/documents/ChoiceAnswer'; +import { ModelMeta } from '@tdev-models/documents/Quiz/ChoiceAnswer'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import ChoiceAnswerDocument from '@tdev-models/documents/ChoiceAnswer'; +import ChoiceAnswerDocument from '@tdev-models/documents/Quiz/ChoiceAnswer'; import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentType'; import Loader from '@tdev-components/Loader'; import { createRandomOrderMap } from '../helpers/shared'; @@ -12,9 +12,9 @@ import { ScoringFunction } from '../helpers/scoring'; import { QuizScore } from '../Feedback'; import useIsBrowser from '@docusaurus/useIsBrowser'; - interface Props { id: string; + questionIds: string[]; readonly?: boolean; hideQuestionNumbers?: boolean; randomizeOptions?: boolean; diff --git a/src/components/documents/ChoiceAnswer/helpers/assessment.ts b/src/components/documents/ChoiceAnswer/helpers/assessment.ts index 01ad5b4e5..bce24131a 100644 --- a/src/components/documents/ChoiceAnswer/helpers/assessment.ts +++ b/src/components/documents/ChoiceAnswer/helpers/assessment.ts @@ -1,7 +1,7 @@ import ChoiceAnswerDocument, { ChoiceAnswerAssessment, ChoiceAnswerCorrectness -} from '@tdev-models/documents/ChoiceAnswer'; +} from '@tdev-models/documents/Quiz/ChoiceAnswer'; import { ScoringFunction } from './scoring'; import _ from 'es-toolkit/compat'; diff --git a/src/components/documents/ChoiceAnswer/helpers/scoring.tsx b/src/components/documents/ChoiceAnswer/helpers/scoring.tsx index 77081cc1f..4366e4600 100644 --- a/src/components/documents/ChoiceAnswer/helpers/scoring.tsx +++ b/src/components/documents/ChoiceAnswer/helpers/scoring.tsx @@ -1,4 +1,4 @@ -import { ChoiceAnswerScoring, ChoiceAnswerCorrectness } from '@tdev-models/documents/ChoiceAnswer'; +import { ChoiceAnswerScoring, ChoiceAnswerCorrectness } from '@tdev-models/documents/Quiz/ChoiceAnswer'; import clsx from 'clsx'; export type ScoringFunction = ( diff --git a/src/models/documents/ChoiceAnswer/index.ts b/src/models/documents/Quiz/ChoiceAnswer.ts similarity index 92% rename from src/models/documents/ChoiceAnswer/index.ts rename to src/models/documents/Quiz/ChoiceAnswer.ts index cd9dbac79..77a43f3f8 100644 --- a/src/models/documents/ChoiceAnswer/index.ts +++ b/src/models/documents/Quiz/ChoiceAnswer.ts @@ -4,6 +4,7 @@ import iDocument, { Source } from '@tdev-models/iDocument'; import DocumentStore from '@tdev-stores/DocumentStore'; import { action, computed, observable } from 'mobx'; import type { ReactElement } from 'react'; +import iAssesable from './iAssessable'; export interface ChoiceAnswerChoices { [questionIndex: number]: number[]; @@ -58,19 +59,17 @@ export class ModelMeta extends TypeMeta<'choice_answer'> { } } -class ChoiceAnswer extends iDocument<'choice_answer'> { +class ChoiceAnswer extends iAssesable<'choice_answer'> { @observable.ref accessor choices: ChoiceAnswerChoices; @observable.ref accessor optionOrders: ChoiceAnswerOptionOrders; @observable.ref accessor questionOrder: ChoiceAnswerQuestionOrder | null; assessments = observable.map(); - @observable accessor _assessed: boolean; constructor(props: DocumentProps<'choice_answer'>, store: DocumentStore) { super(props, store); this.choices = props.data?.choices || {}; this.optionOrders = props.data?.optionOrders || {}; this.questionOrder = props.data?.questionOrder || null; - this._assessed = props.data?.assessed || false; this.assessments = observable.map(); } @@ -89,11 +88,6 @@ class ChoiceAnswer extends iDocument<'choice_answer'> { } } - @computed - get canUpdateAnswer() { - return this.canEdit && !this._assessed; - } - @action updateSingleChoiceSelection(questionIndex: number, optionIndex: number): void { if (!this.canUpdateAnswer) { @@ -167,17 +161,6 @@ class ChoiceAnswer extends iDocument<'choice_answer'> { this.saveNow(); } - get assessed() { - return this._assessed; - } - - @action - set assessed(value: boolean) { - this.updatedAt = new Date(); - this._assessed = value; - this.saveNow(); - } - @action updateAssessment(questionIndex: number, assessment: ChoiceAnswerAssessment): void { this.assessments.set(questionIndex, assessment); diff --git a/src/models/documents/Quiz/iAssessable.ts b/src/models/documents/Quiz/iAssessable.ts new file mode 100644 index 000000000..39273433d --- /dev/null +++ b/src/models/documents/Quiz/iAssessable.ts @@ -0,0 +1,37 @@ +import { AssessableType, Document as DocumentProps } from '@tdev-api/document'; +import iDocument, { Source } from '@tdev-models/iDocument'; +import DocumentStore from '@tdev-stores/DocumentStore'; +import { action, computed, observable } from 'mobx'; + +abstract class iAssesable extends iDocument { + @observable accessor _assessed: boolean; + + constructor(props: DocumentProps, store: DocumentStore) { + super(props, store); + this._assessed = props.data?.assessed || false; + } + + @computed + get assessed() { + return this._assessed; + } + + @action + setAssessed(value: boolean) { + this._assessed = value; + this.saveNow(); + } + + @computed + get canUpdateAnswer() { + return this.canEdit && !this._assessed; + } + + abstract resetAnswer(questionIndex: number): void; + + shuffle(): void { + // By default, do nothing. Only applicable for certain assessable document types (e.g. ChoiceAnswer). + } +} + +export default iAssesable; diff --git a/src/stores/DocumentStore.ts b/src/stores/DocumentStore.ts index 942adb85f..3c35512c4 100644 --- a/src/stores/DocumentStore.ts +++ b/src/stores/DocumentStore.ts @@ -35,7 +35,7 @@ import ProgressState from '@tdev-models/documents/ProgressState'; import Script from '@tdev-models/documents/Code'; import TaskState from '@tdev-models/documents/TaskState'; import Code from '@tdev-models/documents/Code'; -import ChoiceAnswer from '@tdev-models/documents/ChoiceAnswer'; +import ChoiceAnswer from '@tdev-models/documents/Quiz/ChoiceAnswer'; const IsNotUniqueError = (error: any) => { try { From f5ed8fef7015b20f7cb85ed3899aab7f436d6f0b Mon Sep 17 00:00:00 2001 From: bh0fer Date: Tue, 28 Apr 2026 17:29:25 +0200 Subject: [PATCH 06/35] fix typo --- src/models/documents/Quiz/ChoiceAnswer.ts | 4 ++-- src/models/documents/Quiz/iAssessable.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/documents/Quiz/ChoiceAnswer.ts b/src/models/documents/Quiz/ChoiceAnswer.ts index 77a43f3f8..16b4bec83 100644 --- a/src/models/documents/Quiz/ChoiceAnswer.ts +++ b/src/models/documents/Quiz/ChoiceAnswer.ts @@ -4,7 +4,7 @@ import iDocument, { Source } from '@tdev-models/iDocument'; import DocumentStore from '@tdev-stores/DocumentStore'; import { action, computed, observable } from 'mobx'; import type { ReactElement } from 'react'; -import iAssesable from './iAssessable'; +import iAssessable from './iAssessable'; export interface ChoiceAnswerChoices { [questionIndex: number]: number[]; @@ -59,7 +59,7 @@ export class ModelMeta extends TypeMeta<'choice_answer'> { } } -class ChoiceAnswer extends iAssesable<'choice_answer'> { +class ChoiceAnswer extends iAssessable<'choice_answer'> { @observable.ref accessor choices: ChoiceAnswerChoices; @observable.ref accessor optionOrders: ChoiceAnswerOptionOrders; @observable.ref accessor questionOrder: ChoiceAnswerQuestionOrder | null; diff --git a/src/models/documents/Quiz/iAssessable.ts b/src/models/documents/Quiz/iAssessable.ts index 39273433d..ab52d97c0 100644 --- a/src/models/documents/Quiz/iAssessable.ts +++ b/src/models/documents/Quiz/iAssessable.ts @@ -3,7 +3,7 @@ import iDocument, { Source } from '@tdev-models/iDocument'; import DocumentStore from '@tdev-stores/DocumentStore'; import { action, computed, observable } from 'mobx'; -abstract class iAssesable extends iDocument { +abstract class iAssessable extends iDocument { @observable accessor _assessed: boolean; constructor(props: DocumentProps, store: DocumentStore) { @@ -34,4 +34,4 @@ abstract class iAssesable extends iDocument { } } -export default iAssesable; +export default iAssessable; From db5275894396a1e4258cf249fbc1d7eacf9c688b Mon Sep 17 00:00:00 2001 From: bh0fer Date: Mon, 25 May 2026 14:57:37 +0200 Subject: [PATCH 07/35] some progress --- src/api/document.ts | 14 +- .../ChoiceAnswer/Component/index.tsx | 225 +++++++++--------- .../documents/ChoiceAnswer/Controls/index.tsx | 16 +- .../documents/ChoiceAnswer/Feedback/index.tsx | 12 +- .../documents/ChoiceAnswer/Quiz/index.tsx | 43 ++-- .../ChoiceAnswer/helpers/assessment.ts | 6 +- src/hooks/useContextDocumentRootId.tsx | 27 +++ src/hooks/useFirstDocumentBy.ts | 75 ++++++ src/models/DocumentRoot.ts | 12 +- src/models/documents/Quiz/ChoiceAnswer.ts | 149 +++++------- src/models/documents/Quiz/iAssessable.ts | 49 +++- .../tests/plugin.test.ts | 53 +++++ .../answer/choice-answer/index.mdx | 20 +- 13 files changed, 444 insertions(+), 257 deletions(-) create mode 100644 src/hooks/useContextDocumentRootId.tsx create mode 100644 src/hooks/useFirstDocumentBy.ts diff --git a/src/api/document.ts b/src/api/document.ts index cc89b96b5..a5d342a6d 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -19,11 +19,7 @@ import iDocumentContainer from '@tdev-models/iDocumentContainer'; import iViewStore from '@tdev-stores/ViewStores/iViewStore'; import Code from '@tdev-models/documents/Code'; import { iTaskableDocument } from '@tdev-models/iTaskableDocument'; -import ChoiceAnswer, { - ChoiceAnswerChoices, - ChoiceAnswerOptionOrders, - ChoiceAnswerQuestionOrder -} from '@tdev-models/documents/Quiz/ChoiceAnswer'; +import type ChoiceAnswer from '@tdev-models/documents/Quiz/ChoiceAnswer'; export enum Access { RO_DocumentRoot = 'RO_DocumentRoot', @@ -47,7 +43,7 @@ export interface StringData { } export interface QuizData { - // nothing yet, just a wrapper for now + questionOrder: number[]; } export interface BooleanAnswerData { @@ -56,10 +52,10 @@ export interface BooleanAnswerData { } export interface ChoiceAnswerData { - choices: ChoiceAnswerChoices; - optionOrders: ChoiceAnswerOptionOrders; - questionOrder: ChoiceAnswerQuestionOrder | null; + choices: number[]; + optionsOrder: number[]; assessed: boolean; + qid?: string; } export interface QuillV2Data { diff --git a/src/components/documents/ChoiceAnswer/Component/index.tsx b/src/components/documents/ChoiceAnswer/Component/index.tsx index 10fc8df36..21ca57dd2 100644 --- a/src/components/documents/ChoiceAnswer/Component/index.tsx +++ b/src/components/documents/ChoiceAnswer/Component/index.tsx @@ -13,13 +13,19 @@ import useIsBrowser from '@docusaurus/useIsBrowser'; import { QuizContext } from '../Quiz'; import Button from '@tdev-components/shared/Button'; import { mdiTrashCanOutline } from '@mdi/js'; -import { createRandomOrderMap } from '../helpers/shared'; import QuestionControls from '../Controls'; import { FeedbackBadge } from '../Feedback'; import { ScoringFunction } from '../helpers/scoring'; -import { assess } from '../helpers/assessment'; +import { useDocumentRootId } from '@tdev-hooks/useContextDocumentRootId'; +import { useDummyId } from '@tdev-hooks/useDummyId'; +import { useDocumentRoot } from '@tdev-hooks/useDocumentRoot'; +import { useFirstDocumentBy } from '@tdev-hooks/useFirstDocumentBy'; +import { DocumentModelType } from '@tdev-api/document'; +import { DocContext } from '@tdev-components/documents/DocumentContext'; +import { useDocument } from '@tdev-hooks/useContextDocument'; interface SharedProps { + id?: string; title?: string; correct?: number[]; scoring?: ScoringFunction; @@ -33,6 +39,7 @@ interface SharedProps { export interface StandaloneProps extends SharedProps { inQuiz?: false; id: string; + qid: never; } export interface InQuizProps extends SharedProps { @@ -58,61 +65,57 @@ type ChoiceAnswerSubComponents = { After: React.FC; }; -const ChoiceAnswerContext = React.createContext({ - doc: undefined, - questionIndex: 0, - multiple: false, - randomizeOptions: false, - onChange: () => {} -} as { - doc?: ChoiceAnswerDocument; - questionIndex: number; - multiple?: boolean; - randomizeOptions?: boolean; - onChange: (optionIndex: number, checked: boolean) => void; -}); - const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { - const parentProps = React.useContext(QuizContext); const [meta] = React.useState(new ModelMeta(props)); - const ownDoc = useFirstMainDocument(props.inQuiz ? undefined : props.id, meta); - const doc = props.inQuiz ? parentProps.doc : ownDoc; - const questionIndex = props.questionIndex ?? 0; - const randomizeOptions = - props.randomizeOptions !== undefined ? props.randomizeOptions : parentProps.randomizeOptions; + const docRootId = useDocumentRootId(props.id); + // when inside a quizz, this will share the document root with the quiz + const documentRoot = useDocumentRoot(docRootId, meta); + const selector = React.useCallback( + (doc: DocumentModelType) => { + if (props.inQuiz) { + return doc.type === meta.type && doc.data.qid === props.qid; + } + return doc.type === meta.type; + }, + [meta.type, props.inQuiz, props.qid] + ); + + const doc = useFirstDocumentBy(docRootId, meta, selector); const isBrowser = useIsBrowser(); - React.useEffect(() => { - if (randomizeOptions && !doc?.data.optionOrders?.[questionIndex]) { - doc?.updateOptionOrders({ - ...doc.data.optionOrders, - [questionIndex]: createRandomOrderMap(props.optionsCount) - }); - } - }, [randomizeOptions, doc, questionIndex, props.optionsCount]); + // TODO: shuffle + // React.useEffect(() => { + // if (randomizeOptions && !doc?.data.optionOrders?.[questionIndex]) { + // doc?.updateOptionOrders({ + // ...doc.data.optionOrders, + // [questionIndex]: createRandomOrderMap(props.optionsCount) + // }); + // } + // }, [randomizeOptions, doc, questionIndex, props.optionsCount]); - React.useEffect(() => { - if (!doc) { - return; - } + // TODO: assessment + // React.useEffect(() => { + // if (!doc) { + // return; + // } - if (props.correct === undefined) { - // If no correct options are given, we assume that this question doesn't support assessment. - return; - } - const correctOptions = new Set(props.correct); + // if (props.correct === undefined) { + // // If no correct options are given, we assume that this question doesn't support assessment. + // return; + // } + // const correctOptions = new Set(props.correct); - const scoringFunction = props.scoring ?? parentProps.scoring; - const assessment = assess( - doc, - props.multiple ?? false, - questionIndex, - correctOptions, - props.optionsCount, - scoringFunction - ); - doc.updateAssessment(questionIndex, assessment); - }, [doc, doc?.choices, doc?.assessed]); + // const scoringFunction = props.scoring ?? parentProps.scoring; + // const assessment = assess( + // doc, + // props.multiple ?? false, + // questionIndex, + // correctOptions, + // props.optionsCount, + // scoringFunction + // ); + // doc.updateAssessment(questionIndex, assessment); + // }, [doc, doc?.choices, doc?.isAssessed]); if (!doc) { return ; @@ -122,13 +125,13 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { return ; } - const assessment = doc.getAssessment(questionIndex); - const feedbackStyle = { - [styles.correct]: doc.assessed && assessment?.correctness === ChoiceAnswerCorrectness.Correct, - [styles.partiallyCorrect]: - doc.assessed && assessment?.correctness === ChoiceAnswerCorrectness.PartiallyCorrect, - [styles.incorrect]: doc.assessed && assessment?.correctness === ChoiceAnswerCorrectness.Incorrect - }; + // const assessment = doc.getAssessment(questionIndex); + // const feedbackStyle = { + // [styles.correct]: doc.isAssessed && assessment?.correctness === ChoiceAnswerCorrectness.Correct, + // [styles.partiallyCorrect]: + // doc.isAssessed && assessment?.correctness === ChoiceAnswerCorrectness.PartiallyCorrect, + // [styles.incorrect]: doc.isAssessed && assessment?.correctness === ChoiceAnswerCorrectness.Incorrect + // }; const childrenArray = React.Children.toArray(props.children); const beforeBlock = childrenArray.find( @@ -141,58 +144,62 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { (child) => React.isValidElement(child) && child.type === ChoiceAnswer.After ); - const onOptionChange = (optionIndex: number, checked: boolean) => { - parentProps.setFocussedQuestion?.(questionIndex); - if (props.multiple) { - doc?.updateMultipleChoiceSelection(questionIndex, optionIndex, checked); - } else { - checked - ? doc?.updateSingleChoiceSelection(questionIndex, optionIndex) - : doc?.resetAnswer(questionIndex); - } - }; + // const onOptionChange = (optionIndex: number, checked: boolean) => { + // parentProps.setFocussedQuestion?.(questionIndex); + // if (props.multiple) { + // doc?.updateMultipleChoiceSelection(questionIndex, optionIndex, checked); + // } else { + // checked + // ? doc?.updateSingleChoiceSelection(questionIndex, optionIndex) + // : doc?.resetAnswer(questionIndex); + // } + // }; - const questionOrder = - parentProps.randomizeQuestions && parentProps.questionOrder - ? parentProps.questionOrder[questionIndex] - : questionIndex; + // const questionOrder = + // parentProps.randomizeQuestions && parentProps.questionOrder + // ? parentProps.questionOrder[questionIndex] + // : questionIndex; - const questionNumberToDisplay = - (parentProps.randomizeQuestions - ? (parentProps.questionOrder?.[questionIndex] ?? questionIndex) - : questionIndex) + 1; - const canonicalTitle = - props.inQuiz && !parentProps.hideQuestionNumbers - ? props.title - ? `Frage ${questionNumberToDisplay} – ${props.title}` - : `Frage ${questionNumberToDisplay}` - : props.title; + // const questionNumberToDisplay = + // (parentProps.randomizeQuestions + // ? (parentProps.questionOrder?.[questionIndex] ?? questionIndex) + // : questionIndex) + 1; + // const canonicalTitle = + // props.inQuiz && !parentProps.hideQuestionNumbers + // ? props.title + // ? `Frage ${questionNumberToDisplay} – ${props.title}` + // : `Frage ${questionNumberToDisplay}` + // : props.title; + const canonicalTitle = undefined; const displayTitle = canonicalTitle || 'Frage'; return (
-
+
{displayTitle}
- {!!props.correct && ( + {/* {!!props.correct && ( - )} - + )} */} + {/* */}
{beforeBlock} - +
{optionsBlock}
+ + {/* { randomizeOptions: randomizeOptions, onChange: onOptionChange }} - > -
{optionsBlock}
-
+ > */} + {/*
*/} {afterBlock}
@@ -210,41 +216,44 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => { }) as React.FC & ChoiceAnswerSubComponents; ChoiceAnswer.Option = observer(({ optionIndex, children }: OptionProps) => { - const { doc, questionIndex, multiple, randomizeOptions, onChange } = - React.useContext(ChoiceAnswerContext); + const doc = useDocument<'choice_answer'>(); + // const { doc, questionIndex, multiple, randomizeOptions, onChange } = + // React.useContext(ChoiceAnswerContext); const optionId = React.useId(); - const isChecked = !!doc?.choices[questionIndex]?.includes(optionIndex); + const isChecked = doc.choices.has(optionIndex); - const optionOrder = - randomizeOptions && doc?.optionOrders[questionIndex] !== undefined - ? doc.optionOrders[questionIndex][optionIndex] - : optionIndex; + // const optionOrder = + // randomizeOptions && doc?.optionOrders[questionIndex] !== undefined + // ? doc.optionOrders[questionIndex][optionIndex] + // : optionIndex; return (
onChange(optionIndex, e.target.checked)} + onChange={(e) => doc.updateSelection(optionIndex, e.target.checked, doc.multiple)} checked={isChecked} className={styles.checkbox} disabled={!doc?.canUpdateAnswer} - tabIndex={optionOrder} + // tabIndex={optionOrder} />
- {!multiple && ( + {/* {!multiple && (
- )} + )} */}
); }); diff --git a/src/components/documents/ChoiceAnswer/Controls/index.tsx b/src/components/documents/ChoiceAnswer/Controls/index.tsx index 478907150..90ee8c3fc 100644 --- a/src/components/documents/ChoiceAnswer/Controls/index.tsx +++ b/src/components/documents/ChoiceAnswer/Controls/index.tsx @@ -26,7 +26,7 @@ const QuestionControls = observer(({ doc, focussedQuestion: isFocussedQuestion, const checkOrResetButton = !inQuiz && ( <> - {!doc.assessed && ( + {!doc.isAssessed && (
- )} */} + )} ); }); @@ -283,7 +177,11 @@ ChoiceAnswer.Before = ({ children }: { children: React.ReactNode }) => { return <>{children}; }; ChoiceAnswer.Options = ({ children }: { children: React.ReactNode }) => { - return
{children}
; + return ( +
+
{children}
+
+ ); }; ChoiceAnswer.After = ({ children }: { children: React.ReactNode }) => { return <>{children}; diff --git a/src/components/documents/ChoiceAnswer/Component/styles.module.scss b/src/components/documents/ChoiceAnswer/Component/styles.module.scss index c8ca511cf..a10bc4316 100644 --- a/src/components/documents/ChoiceAnswer/Component/styles.module.scss +++ b/src/components/documents/ChoiceAnswer/Component/styles.module.scss @@ -18,7 +18,7 @@ } } - &.partiallyCorrect { + &.partially_correct { box-shadow: $boxShadowDefaults var(--ifm-color-warning); .header { diff --git a/src/components/documents/ChoiceAnswer/Controls/index.tsx b/src/components/documents/ChoiceAnswer/Controls/index.tsx index 3585694c3..74172aa90 100644 --- a/src/components/documents/ChoiceAnswer/Controls/index.tsx +++ b/src/components/documents/ChoiceAnswer/Controls/index.tsx @@ -10,7 +10,6 @@ import useIsMobileView from '@tdev-hooks/useIsMobileView'; interface ControlsProps { doc: ChoiceAnswerDocument; - questionIndex: number; focussedQuestion?: boolean; inQuiz?: boolean; } @@ -19,26 +18,18 @@ const QuestionControls = observer(({ doc, focussedQuestion: isFocussedQuestion, const isMobileView = useIsMobileView(); if (!doc) { - return; + return null; } const syncStatus = isFocussedQuestion && ; - const checkOrResetButton = !inQuiz && ( - <> - {!doc.isAssessed && ( -