@lernapps/app-templates 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +195 -2
- package/dist/appname-Bh23cUuz.mjs +12 -0
- package/dist/assessment-By2GGTJC.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-BmRrD7UY.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-BkEl-m-F.mjs +254 -0
- package/dist/content-cYBPYg_J.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-BiMI-pCL.mjs +89 -0
- package/dist/engine-BqF2Vffn.mjs +136 -0
- package/dist/explainer-e2e.mjs +1297 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2131 -0
- package/dist/explainer.mjs +1246 -0
- package/dist/html-mffkHGHh.mjs +89 -0
- package/dist/image-CspJVfyQ.mjs +21 -0
- package/dist/interactive-e2e.mjs +140 -0
- package/dist/interactive-plugin.mjs +242 -0
- package/dist/interactive.mjs +75 -0
- package/dist/load-C-0hUaSC.mjs +16947 -0
- package/dist/needs-BzEahJ6X.mjs +97 -0
- package/dist/pages-9RaaomoO.mjs +81 -0
- package/dist/questions-e2e.mjs +1 -0
- package/dist/questions-engine.mjs +3 -0
- package/dist/questions.mjs +6 -0
- package/dist/quiz-BKc8JYtC.mjs +70 -0
- package/dist/quiz-e2e.mjs +174 -0
- package/dist/quiz-plugin.mjs +266 -0
- package/dist/quiz.mjs +246 -0
- package/dist/render-BJey0bwl.mjs +540 -0
- package/dist/render-BL6fV_NI.mjs +507 -0
- package/dist/render-DpRO_RKm.mjs +146 -0
- package/dist/result-_eQcamoz.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-COaZso7u.mjs +1072 -0
- package/dist/validate-CxtdeQ5S.mjs +146 -0
- package/dist/vue-DO1m6q1R.mjs +40 -0
- package/dist/vue-code-BkJjW_I_.mjs +320 -0
- package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
- package/docs/arc42/02-constraints.arc42.md +228 -0
- package/docs/arc42/03-system-context.arc42.md +118 -0
- package/docs/arc42/04-solution-strategy.arc42.md +47 -0
- package/docs/arc42/05-building-blocks.arc42.md +307 -0
- package/docs/arc42/06-runtime-view.arc42.md +98 -0
- package/docs/arc42/07-deployment-view.arc42.md +160 -0
- package/docs/arc42/08-concepts.arc42.md +123 -0
- package/docs/arc42/09-decisions.arc42.md +246 -0
- package/docs/arc42/10-quality-requirements.arc42.md +155 -0
- package/docs/arc42/11-risks.arc42.md +113 -0
- package/docs/arc42/12-glossary.arc42.md +162 -0
- package/docs/arc42/architecture-evidence.md +395 -0
- package/explainer/.arc42ignore +3 -0
- package/explainer/README.md +54 -0
- package/explainer/_gitignore +5 -0
- package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
- package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
- package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
- package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
- package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
- package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
- package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
- package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
- package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
- package/explainer/e2e/explainer.e2e.ts +3 -0
- package/explainer/index.html +11 -0
- package/explainer/package.json +18 -0
- package/explainer/playwright.config.ts +1 -0
- package/explainer/src/app.yaml +20 -0
- package/explainer/src/components/ProzentFeld.test.ts +30 -0
- package/explainer/src/components/ProzentFeld.ts +121 -0
- package/explainer/src/components/ProzentFeld.vue +84 -0
- package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
- package/explainer/src/main.ts +4 -0
- package/explainer/src/prozent/chapter.yaml +18 -0
- package/explainer/src/prozent/grundwert.yaml +115 -0
- package/explainer/src/prozent/prozentsatz.yaml +99 -0
- package/explainer/src/prozent/prozentwert.yaml +119 -0
- package/explainer/tsconfig.json +1 -0
- package/explainer/vite.config.ts +4 -0
- package/interactive/.arc42ignore +5 -0
- package/interactive/README.md +28 -0
- package/interactive/_gitignore +4 -0
- package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
- package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
- package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
- package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
- package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
- package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
- package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
- package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
- package/interactive/e2e/interactive.e2e.ts +3 -0
- package/interactive/index.html +11 -0
- package/interactive/package.json +18 -0
- package/interactive/playwright.config.ts +1 -0
- package/interactive/src/app.yaml +28 -0
- package/interactive/src/core/index.ts +101 -0
- package/interactive/src/data/lessons.json +4 -0
- package/interactive/src/env.d.ts +6 -0
- package/interactive/src/main.ts +6 -0
- package/interactive/src/messages/de.json +12 -0
- package/interactive/src/playthroughs/grundreihe.yaml +33 -0
- package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
- package/interactive/src/ui/App.vue +85 -0
- package/interactive/tsconfig.json +1 -0
- package/interactive/vite.config.ts +4 -0
- package/package.json +144 -4
- package/quiz/README.md +31 -0
- package/quiz/_gitignore +5 -0
- package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
- package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
- package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
- package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
- package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
- package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
- package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
- package/quiz/e2e/quiz.e2e.ts +3 -0
- package/quiz/index.html +11 -0
- package/quiz/package.json +18 -0
- package/quiz/playwright.config.ts +1 -0
- package/quiz/src/images/berge-ordnen.jpg +0 -0
- package/quiz/src/images/everest-hoehe.jpg +0 -0
- package/quiz/src/images/galapagos.jpg +0 -0
- package/quiz/src/images/grand-canyon.jpg +0 -0
- package/quiz/src/images/totes-meer.jpg +0 -0
- package/quiz/src/images/uluru.jpg +0 -0
- package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
- package/quiz/src/main.ts +4 -0
- package/quiz/src/quiz.json +312 -0
- package/quiz/tsconfig.json +1 -0
- package/quiz/vite.config.ts +4 -0
- package/runtime/explainer/assessment-client.ts +478 -0
- package/runtime/explainer/assessment-page.ts +446 -0
- package/runtime/explainer/assessment.ts +393 -0
- package/runtime/explainer/check-page.ts +520 -0
- package/runtime/explainer/components/exercise.ts +409 -0
- package/runtime/explainer/components/props.ts +230 -0
- package/runtime/explainer/components/read.ts +428 -0
- package/runtime/explainer/content.ts +277 -0
- package/runtime/explainer/e2e.ts +1474 -0
- package/runtime/explainer/exercises.ts +324 -0
- package/runtime/explainer/explainer.css +1283 -0
- package/runtime/explainer/explainer.ts +838 -0
- package/runtime/explainer/explainer.v1.schema.json +1165 -0
- package/runtime/explainer/expression.ts +407 -0
- package/runtime/explainer/fraction.ts +107 -0
- package/runtime/explainer/generated/comparator.ts +31 -0
- package/runtime/explainer/generated/question.ts +165 -0
- package/runtime/explainer/generated/soundness.ts +280 -0
- package/runtime/explainer/generated/texts.ts +257 -0
- package/runtime/explainer/generated/types.ts +103 -0
- package/runtime/explainer/generated/values.ts +136 -0
- package/runtime/explainer/islands.ts +281 -0
- package/runtime/explainer/load.ts +457 -0
- package/runtime/explainer/messages/de.json +200 -0
- package/runtime/explainer/messages/en.json +200 -0
- package/runtime/explainer/messages.ts +13 -0
- package/runtime/explainer/needs.ts +122 -0
- package/runtime/explainer/open-task.ts +235 -0
- package/runtime/explainer/outline.ts +299 -0
- package/runtime/explainer/plugin.ts +488 -0
- package/runtime/explainer/position.ts +62 -0
- package/runtime/explainer/prompt.ts +75 -0
- package/runtime/explainer/render.ts +686 -0
- package/runtime/explainer/result.ts +238 -0
- package/runtime/explainer/routes.ts +119 -0
- package/runtime/explainer/schema.ts +140 -0
- package/runtime/explainer/scope.ts +87 -0
- package/runtime/explainer/stuck.ts +57 -0
- package/runtime/explainer/tasks.ts +109 -0
- package/runtime/explainer/tutor.ts +469 -0
- package/runtime/explainer/tutorpage.ts +669 -0
- package/runtime/explainer/video.ts +123 -0
- package/runtime/interactive/contract.ts +121 -0
- package/runtime/interactive/e2e.ts +190 -0
- package/runtime/interactive/interactive.css +82 -0
- package/runtime/interactive/interactive.ts +115 -0
- package/runtime/interactive/interactive.v1.schema.json +146 -0
- package/runtime/interactive/load.ts +188 -0
- package/runtime/interactive/messages/de.json +14 -0
- package/runtime/interactive/messages/en.json +14 -0
- package/runtime/interactive/messages.ts +11 -0
- package/runtime/interactive/playthrough.v1.schema.json +143 -0
- package/runtime/interactive/plugin.ts +266 -0
- package/runtime/interactive/render.ts +126 -0
- package/runtime/quiz/e2e.ts +233 -0
- package/runtime/quiz/engine.ts +242 -0
- package/runtime/quiz/messages/de.json +23 -0
- package/runtime/quiz/messages/en.json +23 -0
- package/runtime/quiz/plugin.ts +150 -0
- package/runtime/quiz/quiz.css +154 -0
- package/runtime/quiz/quiz.ts +56 -0
- package/runtime/quiz/quiz.v1.schema.json +72 -0
- package/runtime/quiz/render.ts +173 -0
- package/runtime/quiz/validate.ts +108 -0
- package/runtime/shared/appname.ts +16 -0
- package/runtime/shared/blocks/block.ts +162 -0
- package/runtime/shared/blocks/blocks.css +190 -0
- package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
- package/runtime/shared/blocks/code.ts +26 -0
- package/runtime/shared/blocks/figures.ts +49 -0
- package/runtime/shared/blocks/image.ts +25 -0
- package/runtime/shared/blocks/index.ts +13 -0
- package/runtime/shared/blocks/messages/de.json +4 -0
- package/runtime/shared/blocks/messages/en.json +4 -0
- package/runtime/shared/blocks/render.ts +134 -0
- package/runtime/shared/blocks/validate.ts +165 -0
- package/runtime/shared/content.ts +197 -0
- package/runtime/shared/examples.ts +20 -0
- package/runtime/shared/html.ts +10 -0
- package/runtime/shared/languages.ts +94 -0
- package/runtime/shared/page.css +101 -0
- package/runtime/shared/pages.ts +114 -0
- package/runtime/shared/questions/e2e.ts +115 -0
- package/runtime/shared/questions/engine.ts +209 -0
- package/runtime/shared/questions/index.ts +39 -0
- package/runtime/shared/questions/messages/de.json +26 -0
- package/runtime/shared/questions/messages/en.json +26 -0
- package/runtime/shared/questions/question.ts +430 -0
- package/runtime/shared/questions/question.v1.schema.json +342 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +247 -0
- package/runtime/shared/questions/validate.ts +518 -0
- package/runtime/shared/schema.ts +44 -0
- package/runtime/shared/text/check.ts +68 -0
- package/runtime/shared/text/formula.ts +69 -0
- package/runtime/shared/text/markdown.ts +185 -0
- package/runtime/shared/text/text.v1.schema.json +34 -0
- package/runtime/shared/vue-code.ts +395 -0
- package/runtime/shared/vue.ts +38 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// runtime/interactive/contract.ts
|
|
2
|
+
/** The milliseconds from one tick to the next: the runtime calls tick(state, TICK) every TICK ms. */
|
|
3
|
+
const TICK = 20;
|
|
4
|
+
/** The property of the app's element that holds the state, for the end-to-end tests. */
|
|
5
|
+
const STATE_PROPERTY = "lernappsState";
|
|
6
|
+
/**
|
|
7
|
+
* The global of the end-to-end tests' clock: when the page has it (an object, set before the client module runs),
|
|
8
|
+
* the runtime starts no interval and puts on it tick(count), which calls tick(state, TICK) count times, the same as
|
|
9
|
+
* count intervals would. Only the tests set it; for a learner, time passes on its own.
|
|
10
|
+
*/
|
|
11
|
+
const CLOCK_PROPERTY = "lernappsClock";
|
|
12
|
+
/** The inputs of a step, its keys as key inputs. */
|
|
13
|
+
function inputsOf(step) {
|
|
14
|
+
return step.inputs !== void 0 ? [...step.inputs] : (step.keys ?? []).map((key) => ({
|
|
15
|
+
kind: "key",
|
|
16
|
+
key
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/** An input as a playthrough's problem names it: a key as its JSON text, an action with its name and value. */
|
|
20
|
+
function inputText(input) {
|
|
21
|
+
if (input.kind === "key") return JSON.stringify(input.key);
|
|
22
|
+
return `action ${JSON.stringify(input.name)}${input.value === void 0 ? "" : ` ${JSON.stringify(input.value)}`}`;
|
|
23
|
+
}
|
|
24
|
+
/** What a step does, as a playthrough's problem names it: its inputs, then its ticks. */
|
|
25
|
+
function doingText(step) {
|
|
26
|
+
const ticks = step.ticks === void 0 ? [] : [`${step.ticks} tick${step.ticks === 1 ? "" : "s"}`];
|
|
27
|
+
return [...inputsOf(step).map(inputText), ...ticks];
|
|
28
|
+
}
|
|
29
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30
|
+
/**
|
|
31
|
+
* Where `actual` differs from `expected`: the path of the first difference, with the value found there, or undefined
|
|
32
|
+
* when it matches. An object expects only the fields it names (each compared in the same way); anything else, a list
|
|
33
|
+
* too, is compared as a whole.
|
|
34
|
+
*/
|
|
35
|
+
function mismatch(actual, expected, path = []) {
|
|
36
|
+
if (isRecord(expected)) {
|
|
37
|
+
if (!isRecord(actual)) return {
|
|
38
|
+
path: [...path],
|
|
39
|
+
actual
|
|
40
|
+
};
|
|
41
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
42
|
+
const found = mismatch(actual[key], value, [...path, key]);
|
|
43
|
+
if (found !== void 0) return found;
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return JSON.stringify(actual) === JSON.stringify(expected) ? void 0 : {
|
|
48
|
+
path: [...path],
|
|
49
|
+
actual
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export { inputText as a, doingText as i, STATE_PROPERTY as n, inputsOf as o, TICK as r, mismatch as s, CLOCK_PROPERTY as t };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { l as textsIn } from "./html-mffkHGHh.mjs";
|
|
2
|
+
import { g as questionId, t as QUESTION_MESSAGES } from "./render-BJey0bwl.mjs";
|
|
3
|
+
import { expect } from "playwright/test";
|
|
4
|
+
// runtime/shared/questions/e2e.ts
|
|
5
|
+
/** The question's own texts in `lang`. */
|
|
6
|
+
const messagesOf = (lang) => textsIn(QUESTION_MESSAGES, lang);
|
|
7
|
+
const question = (page, item) => page.locator(`#${questionId(item)}`);
|
|
8
|
+
/** Gives `answer` to the question through its controls, as a learner does, on a page in `lang`. */
|
|
9
|
+
async function answerQuestion(page, item, answer, lang = "de") {
|
|
10
|
+
await giveAnswer(page, item, answer);
|
|
11
|
+
await question(page, item).getByRole("button", { name: messagesOf(lang).check }).click();
|
|
12
|
+
}
|
|
13
|
+
/** Puts `answer` into the controls of the question, as a learner does, without checking it. */
|
|
14
|
+
async function giveAnswer(page, item, answer) {
|
|
15
|
+
const form = question(page, item);
|
|
16
|
+
switch (item.type) {
|
|
17
|
+
case "single-choice":
|
|
18
|
+
case "multiple-choice":
|
|
19
|
+
case "true-false": {
|
|
20
|
+
const values = typeof answer === "object" ? answer.map(String) : [String(answer)];
|
|
21
|
+
for (const value of values) await form.locator(`input[value="${value}"]`).check();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case "number":
|
|
25
|
+
await form.locator("input").fill(String(answer));
|
|
26
|
+
break;
|
|
27
|
+
case "ordering":
|
|
28
|
+
case "matching": {
|
|
29
|
+
if (typeof answer !== "object") throw new Error(`${item.id}: an ${item.type} answer is a list`);
|
|
30
|
+
const key = item.type === "ordering" ? "item" : "left";
|
|
31
|
+
for (const [index, value] of answer.entries()) await form.locator(`select[data-${key}="${index}"]`).selectOption(String(value));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** What the learner learns about a question shows in `where`: each option's background, the explanation, the links. */
|
|
37
|
+
async function expectBackground(where, item) {
|
|
38
|
+
if (item.type === "single-choice" || item.type === "multiple-choice") for (const option of item.options) {
|
|
39
|
+
await expect(where.getByText(option.info, { exact: true })).toBeVisible();
|
|
40
|
+
if (option.link) await expect(where.locator(`a[href="${option.link.url}"]`).first()).toBeVisible();
|
|
41
|
+
}
|
|
42
|
+
await expect(where.getByText(item.explanation, { exact: true })).toBeVisible();
|
|
43
|
+
for (const link of item.links) {
|
|
44
|
+
const anchor = where.locator(`a[href="${link.url}"]`).first();
|
|
45
|
+
await expect(anchor).toBeVisible();
|
|
46
|
+
await expect(anchor).toHaveAttribute("rel", "noopener");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** After an answer, the learner sees the feedback without scrolling back; it has the focus, the next button follows. */
|
|
50
|
+
async function expectFeedbackInView(page, item) {
|
|
51
|
+
const feedback = question(page, item).locator(".question-feedback");
|
|
52
|
+
const top = await feedback.evaluate((element) => element.getBoundingClientRect().top);
|
|
53
|
+
const height = await page.evaluate(() => innerHeight);
|
|
54
|
+
expect(top, "the top of the feedback is in the viewport").toBeGreaterThanOrEqual(0);
|
|
55
|
+
expect(top, "the top of the feedback is in the viewport").toBeLessThan(height);
|
|
56
|
+
await expect(feedback).toBeFocused();
|
|
57
|
+
}
|
|
58
|
+
/** What a question shows besides its text (its material) is on the page, between the text and the answers; images load. */
|
|
59
|
+
async function expectMaterial(page, item, { loaded = true } = {}) {
|
|
60
|
+
const blocks = item.material ?? [];
|
|
61
|
+
const area = question(page, item).locator(".question-material");
|
|
62
|
+
if (blocks.length === 0) {
|
|
63
|
+
await expect(area).toHaveCount(0);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await expect(area).toBeVisible();
|
|
67
|
+
const count = (type) => blocks.filter((block) => block.type === type).length;
|
|
68
|
+
for (const [type, selector] of [
|
|
69
|
+
["text", ".block-text"],
|
|
70
|
+
["formula", ".block-formula"],
|
|
71
|
+
["figure", ".block-figure:not(.block-labelled)"],
|
|
72
|
+
["labelled-figure", ".block-labelled"],
|
|
73
|
+
["table", ".block-table"],
|
|
74
|
+
["quote", ".block-quote"],
|
|
75
|
+
["code", ".block-code"]
|
|
76
|
+
]) await expect(area.locator(selector), `${item.id}: the ${type} blocks`).toHaveCount(count(type));
|
|
77
|
+
const withCredit = blocks.filter((block) => "licence" in block && block.licence !== void 0).length;
|
|
78
|
+
await expect(area.locator(".block-credit")).toHaveCount(withCredit);
|
|
79
|
+
const images = blocks.filter((block) => "image" in block && block.image !== void 0);
|
|
80
|
+
const pictures = area.locator("img");
|
|
81
|
+
await expect(pictures).toHaveCount(images.length);
|
|
82
|
+
for (const [index, block] of images.entries()) {
|
|
83
|
+
const picture = pictures.nth(index);
|
|
84
|
+
await expect(picture).toHaveAttribute("alt", block.type === "figure" || block.type === "labelled-figure" ? block.alt : "");
|
|
85
|
+
if (!loaded) continue;
|
|
86
|
+
await expect.poll(() => picture.evaluate((element) => element.complete && element.naturalWidth > 0), { message: `${item.id}: image ${index + 1} loads` }).toBe(true);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export { giveAnswer as a, expectMaterial as i, expectBackground as n, question as o, expectFeedbackInView as r, answerQuestion as t };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { l as textsIn } from "./html-mffkHGHh.mjs";
|
|
2
|
+
import { S as shuffled, c as hash, f as missesRight, l as isComplete, t as QUESTION_MESSAGES, u as isCorrect, w as wrongReasons } from "./render-BJey0bwl.mjs";
|
|
3
|
+
import { translator } from "@lernapps/tooling/i18n";
|
|
4
|
+
import { moveFocus } from "@lernapps/tooling/a11y";
|
|
5
|
+
// runtime/shared/questions/engine.ts
|
|
6
|
+
/** The learner's answer as the form holds it; undefined when nothing is given yet. */
|
|
7
|
+
function readAnswer(question, form) {
|
|
8
|
+
const checked = [...form.querySelectorAll("input:checked")].map((input) => input.value);
|
|
9
|
+
switch (question.type) {
|
|
10
|
+
case "single-choice": return checked[0] === void 0 ? void 0 : Number(checked[0]);
|
|
11
|
+
case "multiple-choice": return checked.map(Number);
|
|
12
|
+
case "true-false": return checked[0] === void 0 ? void 0 : checked[0] === "true";
|
|
13
|
+
case "number": return form.querySelector("input")?.value ?? "";
|
|
14
|
+
case "ordering":
|
|
15
|
+
case "matching": {
|
|
16
|
+
const key = question.type === "ordering" ? "item" : "left";
|
|
17
|
+
const answer = [];
|
|
18
|
+
for (const select of form.querySelectorAll(`select[data-${key}]`)) answer[Number(select.dataset[key])] = select.value === "" ? -1 : Number(select.value);
|
|
19
|
+
return answer;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** The indices of the options the learner chose, for the question types with options. */
|
|
24
|
+
function chosenOptions(question, answer) {
|
|
25
|
+
if (question.type !== "single-choice" && question.type !== "multiple-choice") return [];
|
|
26
|
+
return typeof answer === "number" ? [answer] : typeof answer === "object" ? [...answer] : [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Makes one rendered question (`item`, the element of renderQuestion) live: the button to check it shows; a complete
|
|
30
|
+
* answer gets its feedback, with a wrong one the most specific reason the question gives (the diagnosis, the
|
|
31
|
+
* background of each wrong option chosen, a number's typical mistake, else its hint). Then the solution shows: the
|
|
32
|
+
* background of every option with the right ones and the learner's choice marked, the explanation and further
|
|
33
|
+
* reading; the controls are locked, the button to the next question shows if there is one, and the focus moves to the
|
|
34
|
+
* feedback. With `retry`, a wrong answer keeps the controls open and offers "Lösung zeigen" instead. An incomplete
|
|
35
|
+
* answer is asked to be completed.
|
|
36
|
+
*/
|
|
37
|
+
function startQuestion(item, question, options = {}) {
|
|
38
|
+
const t = translator(textsIn(QUESTION_MESSAGES, options.lang ?? "de"));
|
|
39
|
+
const form = item.querySelector("form");
|
|
40
|
+
const feedback = item.querySelector(".question-feedback");
|
|
41
|
+
const check = item.querySelector(".question-check");
|
|
42
|
+
const next = item.querySelector(".question-next");
|
|
43
|
+
if (!form || !feedback || !check) return;
|
|
44
|
+
const doc = item.ownerDocument;
|
|
45
|
+
const fieldset = item.querySelector("fieldset");
|
|
46
|
+
if (options.seed !== void 0 && fieldset && (question.type === "single-choice" || question.type === "multiple-choice")) {
|
|
47
|
+
const labels = [...fieldset.querySelectorAll(".question-option")];
|
|
48
|
+
for (const index of shuffled(labels.length, options.seed ^ hash(question.id))) {
|
|
49
|
+
const label = labels[index];
|
|
50
|
+
if (label) fieldset.append(label);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const actions = item.querySelector(".question-actions");
|
|
54
|
+
if (actions) actions.hidden = false;
|
|
55
|
+
let solve;
|
|
56
|
+
if (options.retry === true) {
|
|
57
|
+
solve = doc.createElement("button");
|
|
58
|
+
solve.type = "button";
|
|
59
|
+
solve.className = "question-solve";
|
|
60
|
+
solve.textContent = t("showSolution");
|
|
61
|
+
solve.hidden = true;
|
|
62
|
+
check.after(" ", solve);
|
|
63
|
+
}
|
|
64
|
+
let said = [];
|
|
65
|
+
const line = (text, option) => {
|
|
66
|
+
said.push(option === void 0 ? text : `${option}: ${text}`);
|
|
67
|
+
const why = doc.createElement("p");
|
|
68
|
+
why.className = "question-diagnosis";
|
|
69
|
+
if (option !== void 0) {
|
|
70
|
+
const name = doc.createElement("strong");
|
|
71
|
+
if (options.format) name.innerHTML = `${options.format(option)}:`;
|
|
72
|
+
else name.textContent = `${option}:`;
|
|
73
|
+
why.append(name, " ");
|
|
74
|
+
}
|
|
75
|
+
if (options.format) why.insertAdjacentHTML("beforeend", options.format(text));
|
|
76
|
+
else why.append(text);
|
|
77
|
+
feedback.append(why);
|
|
78
|
+
};
|
|
79
|
+
let answered = false;
|
|
80
|
+
let last;
|
|
81
|
+
/** The solution: every option's background, the right ones marked, the learner's last choice marked too. */
|
|
82
|
+
const reveal = () => {
|
|
83
|
+
answered = true;
|
|
84
|
+
const solution = item.querySelector(".question-reveal");
|
|
85
|
+
for (const index of last === void 0 ? [] : chosenOptions(question, last)) {
|
|
86
|
+
const option = solution?.querySelector(`[data-option="${index}"]`);
|
|
87
|
+
if (!option) continue;
|
|
88
|
+
option.classList.add("question-info-chosen");
|
|
89
|
+
const badge = doc.createElement("span");
|
|
90
|
+
badge.className = "question-badge question-badge-chosen";
|
|
91
|
+
badge.textContent = t("yourChoice");
|
|
92
|
+
option.querySelector(".question-info-head")?.append(" ", badge);
|
|
93
|
+
}
|
|
94
|
+
if (solution) solution.hidden = false;
|
|
95
|
+
for (const control of form.querySelectorAll("input, select")) control.disabled = true;
|
|
96
|
+
check.hidden = true;
|
|
97
|
+
if (solve) solve.hidden = true;
|
|
98
|
+
if (next) next.hidden = false;
|
|
99
|
+
options.onReveal?.();
|
|
100
|
+
};
|
|
101
|
+
form.addEventListener("submit", (event) => {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
if (answered) return;
|
|
104
|
+
const answer = readAnswer(question, form);
|
|
105
|
+
if (!isComplete(question, answer)) {
|
|
106
|
+
feedback.textContent = t("incomplete");
|
|
107
|
+
feedback.className = "question-feedback";
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
last = answer;
|
|
111
|
+
said = [];
|
|
112
|
+
const right = isCorrect(question, answer);
|
|
113
|
+
feedback.className = `question-feedback ${right ? "question-right" : "question-wrong"}`;
|
|
114
|
+
feedback.textContent = t(right ? "right" : "wrong");
|
|
115
|
+
if (!right) {
|
|
116
|
+
const diagnosis = options.diagnose?.(answer);
|
|
117
|
+
const reasons = diagnosis !== void 0 ? [{ text: diagnosis }] : wrongReasons(question, answer).filter((reason) => options.retry === true || reason.option === void 0);
|
|
118
|
+
for (const reason of reasons) line(reason.text, reason.option);
|
|
119
|
+
const missing = missesRight(question, answer);
|
|
120
|
+
if (diagnosis === void 0 && missing) line(t("missingRight"));
|
|
121
|
+
if (options.retry === true && reasons.length === 0 && !missing) line(t("lookAgain"));
|
|
122
|
+
}
|
|
123
|
+
const told = said.join(" ");
|
|
124
|
+
if (!right && options.retry === true) line(t("tryAgain"));
|
|
125
|
+
options.onAnswer?.(answer, right, told);
|
|
126
|
+
if (right || options.retry !== true) reveal();
|
|
127
|
+
else if (solve) solve.hidden = false;
|
|
128
|
+
moveFocus(feedback);
|
|
129
|
+
});
|
|
130
|
+
solve?.addEventListener("click", () => {
|
|
131
|
+
if (answered) return;
|
|
132
|
+
reveal();
|
|
133
|
+
moveFocus(item.querySelector(".question-reveal") ?? feedback);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export { startQuestion as n, readAnswer as t };
|