@lernapps/app-templates 0.0.1 → 0.1.1
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-Dy359O1O.mjs +1696 -0
- package/dist/block-BK_ohsH1.mjs +82 -0
- package/dist/blocks-CxtCPAa5.mjs +149 -0
- package/dist/blocks.mjs +4 -0
- package/dist/check-CteIFN96.mjs +254 -0
- package/dist/content-DO6oEJxj.mjs +140 -0
- package/dist/contract-PONpxJtd.mjs +52 -0
- package/dist/e2e-CU0n_unV.mjs +89 -0
- package/dist/engine-CArxqizW.mjs +136 -0
- package/dist/explainer-e2e.mjs +1311 -0
- package/dist/explainer-islands.mjs +197 -0
- package/dist/explainer-plugin.mjs +2132 -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-RosvaPZf.mjs +16976 -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-DnW-xtCh.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-D5kwkNt1.mjs +507 -0
- package/dist/render-DqHOJyHK.mjs +146 -0
- package/dist/render-GS1Htlu8.mjs +545 -0
- package/dist/result-D_mmCVwN.mjs +180 -0
- package/dist/tasks-DADUYy52.mjs +74 -0
- package/dist/validate-BDwL7Su3.mjs +1076 -0
- package/dist/validate-Bxy9NfB8.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 +143 -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 +448 -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 +1488 -0
- package/runtime/explainer/exercises.ts +326 -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 +306 -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 +433 -0
- package/runtime/shared/questions/question.v1.schema.json +346 -0
- package/runtime/shared/questions/questions.css +192 -0
- package/runtime/shared/questions/render.ts +255 -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/speech.ts +39 -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,281 @@
|
|
|
1
|
+
// The islands of an explainer page in the browser: only a page with a vue-component block loads this module, and
|
|
2
|
+
// with it Vue (plugin.ts adds it to that page alone). Each component loads when its page has it, and hydrates the
|
|
3
|
+
// markup the build rendered into its island, with the props of the page's language. Then its island shows, and its
|
|
4
|
+
// nojs text goes, or, when the component declares defineOptions({ nojs: "caption" }), stays as a text description
|
|
5
|
+
// folded into a <details> under it. An exercise component is the input of its task: it emits answer with the
|
|
6
|
+
// learner's answer each time it changes. The explainer checks it with "Prüfen" under the component (a form, so Enter in
|
|
7
|
+
// a field of the component checks too, enabled while there is an answer that is not checked yet): check, reason and
|
|
8
|
+
// show of the component's logic module give whether it is right, the app's message for a wrong one and the answer as
|
|
9
|
+
// text. The island shows the feedback, with the message under it, in view with the focus, gives the component
|
|
10
|
+
// checked: true until the answer changes, when it declares that prop, and passes the answer on to the client module as
|
|
11
|
+
// the event explainer-answered on its element ({ correct, answer, message }: answer is what the learner gave, as text,
|
|
12
|
+
// for the result line of "Zurück zum Tutor", result.ts; message the app's message, for the lines of "Ich verstehe das
|
|
13
|
+
// nicht", stuck.ts).
|
|
14
|
+
// An exercise component gets the seed of its task number as the prop seed, from the table of its distinct tasks the
|
|
15
|
+
// build ships (data-seeds; tasks.ts): the build rendered task number 1, it hydrates with it, and then moves to the
|
|
16
|
+
// number the address names (?<id>=<n>, wrapped into 1 to N) or to a random one; "Aufgabe Nr. <n>" shows above
|
|
17
|
+
// it, and "Neue Aufgabe" moves to the next number, writes it into the address and mounts the component afresh. Nothing is stored.
|
|
18
|
+
// In a self-assessment (data-assessment) an exercise component has no "Prüfen", no feedback and no task number: it
|
|
19
|
+
// shows the task of the seed the self-assessment gives (data-seed, then ASSESSMENT_TASK_EVENT, each mounting it
|
|
20
|
+
// afresh) and judges each answer as it changes, silently, for the self-assessment's "Auswerten"
|
|
21
|
+
// (ASSESSMENT_ANSWER_EVENT). Its elements are in a <template> until the self-assessment puts them into the page
|
|
22
|
+
// (ASSESSMENT_UNPACKED_EVENT); the islands hydrate them then.
|
|
23
|
+
import { createSSRApp, h, nextTick, ref, shallowRef, type Component } from "vue";
|
|
24
|
+
import {
|
|
25
|
+
addressOf,
|
|
26
|
+
ASSESSMENT_ANSWER_EVENT,
|
|
27
|
+
ASSESSMENT_TASK_EVENT,
|
|
28
|
+
ASSESSMENT_UNPACKED_EVENT,
|
|
29
|
+
nextTaskNumber,
|
|
30
|
+
numberFromAddress,
|
|
31
|
+
randomTaskNumber,
|
|
32
|
+
SHOWN_EVENT,
|
|
33
|
+
taskNumber,
|
|
34
|
+
type AssessmentAnswer,
|
|
35
|
+
} from "./tasks.ts";
|
|
36
|
+
|
|
37
|
+
/** The components of the app by name, each loaded on first use. */
|
|
38
|
+
export type Components = Readonly<Record<string, () => Promise<{ default: Component }>>>;
|
|
39
|
+
|
|
40
|
+
/** What the islands call of an exercise component's logic module (exercise.ts). */
|
|
41
|
+
export interface Logic {
|
|
42
|
+
generate(seed: number, props: unknown): unknown;
|
|
43
|
+
check(task: unknown, answer: unknown): unknown;
|
|
44
|
+
reason?(task: unknown, answer: unknown, props: unknown, lang: string): unknown;
|
|
45
|
+
show?(answer: unknown, task: unknown, props: unknown, lang: string): unknown;
|
|
46
|
+
/** The answer of each typical mistake, in the order of about.mistakes. */
|
|
47
|
+
mistakes?(task: unknown, props: unknown): unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The logic modules of the app's exercise components by name, each loaded on first use. */
|
|
51
|
+
export type Logics = Readonly<Record<string, () => Promise<Logic>>>;
|
|
52
|
+
|
|
53
|
+
/** A text that is not empty, trimmed, or undefined. */
|
|
54
|
+
const textOf = (value: unknown) => (typeof value === "string" && value.trim() !== "" ? value.trim() : undefined);
|
|
55
|
+
|
|
56
|
+
/** The feedback on an answer, with the reason of a wrong one, in place of the one before, with the focus. */
|
|
57
|
+
function showFeedback(feedback: HTMLElement, correct: boolean, reason: unknown): void {
|
|
58
|
+
feedback.className = `question-feedback ${correct ? "question-right" : "question-wrong"}`;
|
|
59
|
+
feedback.textContent = (correct ? feedback.dataset["right"] : feedback.dataset["wrong"]) ?? "";
|
|
60
|
+
if (!correct && typeof reason === "string" && reason.trim() !== "") {
|
|
61
|
+
const why = feedback.ownerDocument.createElement("span");
|
|
62
|
+
why.className = "question-diagnosis";
|
|
63
|
+
why.textContent = reason;
|
|
64
|
+
feedback.append(" ", why);
|
|
65
|
+
}
|
|
66
|
+
feedback.tabIndex = -1;
|
|
67
|
+
feedback.focus();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The nojs text as a text description under the running component: folded, one click opens it. */
|
|
71
|
+
function foldCaption(element: HTMLElement, nojs: HTMLElement): void {
|
|
72
|
+
const doc = element.ownerDocument;
|
|
73
|
+
const details = doc.createElement("details");
|
|
74
|
+
const summary = doc.createElement("summary");
|
|
75
|
+
summary.textContent = element.dataset["caption"] ?? "";
|
|
76
|
+
details.append(summary, ...nojs.childNodes);
|
|
77
|
+
nojs.replaceChildren(details);
|
|
78
|
+
nojs.classList.add("explainer-caption");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The index of the typical mistake `answer` makes, from 0, compared as JSON; undefined for none. */
|
|
82
|
+
function mistakeOf(logic: Logic, task: unknown, answer: unknown, props: unknown): number | undefined {
|
|
83
|
+
const mistakes = logic.mistakes?.(task, props);
|
|
84
|
+
if (!Array.isArray(mistakes)) return undefined;
|
|
85
|
+
const index = mistakes.findIndex(
|
|
86
|
+
(mistake) => mistake !== undefined && JSON.stringify(mistake) === JSON.stringify(answer),
|
|
87
|
+
);
|
|
88
|
+
return index < 0 ? undefined : index;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mounts an exercise component of a self-assessment: the task of the seed it gives, each answer judged as it changes
|
|
93
|
+
* and sent on, no "Prüfen".
|
|
94
|
+
*/
|
|
95
|
+
function assess(
|
|
96
|
+
element: HTMLElement,
|
|
97
|
+
island: HTMLElement,
|
|
98
|
+
component: Component,
|
|
99
|
+
props: Record<string, unknown>,
|
|
100
|
+
logic: Logic,
|
|
101
|
+
): void {
|
|
102
|
+
const lang = element.ownerDocument.documentElement.lang || "de";
|
|
103
|
+
const seeds = JSON.parse(element.dataset["seeds"] ?? "[1]") as number[];
|
|
104
|
+
// hydrates with the task of number 1, as the build rendered it, then shows the task the self-assessment gave
|
|
105
|
+
const seed = ref(seeds[0] ?? 1);
|
|
106
|
+
// each task the self-assessment gives mounts it afresh, with no answer, the same seed again too
|
|
107
|
+
const round = ref(0);
|
|
108
|
+
const declaresChecked = element.dataset["checked"] !== undefined;
|
|
109
|
+
/** What the answer the component has now gives: null for none, an incomplete one, or one its logic cannot read. */
|
|
110
|
+
const judge = (answer: unknown): AssessmentAnswer | null => {
|
|
111
|
+
if (answer === undefined) return null;
|
|
112
|
+
try {
|
|
113
|
+
const task = logic.generate(seed.value, props);
|
|
114
|
+
const correct = logic.check(task, answer) === true;
|
|
115
|
+
const message = correct ? undefined : textOf(logic.reason?.(task, answer, props, lang));
|
|
116
|
+
const shown = logic.show === undefined ? undefined : textOf(logic.show(answer, task, props, lang));
|
|
117
|
+
const mistake = correct ? undefined : mistakeOf(logic, task, answer, props);
|
|
118
|
+
return {
|
|
119
|
+
correct,
|
|
120
|
+
...(shown === undefined ? {} : { answer: shown }),
|
|
121
|
+
...(message === undefined ? {} : { message }),
|
|
122
|
+
...(mistake === undefined ? {} : { mistake }),
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const onAnswer = (answer: unknown) => {
|
|
129
|
+
element.dispatchEvent(new CustomEvent(ASSESSMENT_ANSWER_EVENT, { bubbles: true, detail: judge(answer) }));
|
|
130
|
+
};
|
|
131
|
+
const app = createSSRApp({
|
|
132
|
+
render: () =>
|
|
133
|
+
h(component, {
|
|
134
|
+
...props,
|
|
135
|
+
seed: seed.value,
|
|
136
|
+
key: round.value,
|
|
137
|
+
...(declaresChecked ? { checked: false } : {}),
|
|
138
|
+
onAnswer,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
// ids of its own (useId): several components share a page of the self-assessment, each a Vue app of its own
|
|
142
|
+
app.config.idPrefix = element.id || "aufgabe";
|
|
143
|
+
app.mount(island);
|
|
144
|
+
const given = Number(element.dataset["seed"]);
|
|
145
|
+
if (given > 0) {
|
|
146
|
+
seed.value = given;
|
|
147
|
+
round.value += 1;
|
|
148
|
+
}
|
|
149
|
+
element.addEventListener(ASSESSMENT_TASK_EVENT, (event) => {
|
|
150
|
+
seed.value = (event as CustomEvent<{ seed: number }>).detail.seed;
|
|
151
|
+
round.value += 1;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Hydrates one component in place, with the logic module of an exercise component. */
|
|
156
|
+
async function hydrate(
|
|
157
|
+
element: HTMLElement,
|
|
158
|
+
load: () => Promise<{ default: Component }>,
|
|
159
|
+
loadLogic: (() => Promise<Logic>) | undefined,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
const island = element.querySelector<HTMLElement>(".explainer-island");
|
|
162
|
+
const nojs = element.querySelector<HTMLElement>(":scope > .explainer-nojs");
|
|
163
|
+
if (!island) return;
|
|
164
|
+
const props = JSON.parse(
|
|
165
|
+
element.querySelector(":scope > script.explainer-component-props")?.textContent ?? "{}",
|
|
166
|
+
) as Record<string, unknown>;
|
|
167
|
+
const feedback = element.querySelector<HTMLElement>(":scope > .question-feedback");
|
|
168
|
+
const { default: component } = await load();
|
|
169
|
+
if (element.dataset["exercise"] === undefined || loadLogic === undefined) {
|
|
170
|
+
createSSRApp(component, props).mount(island);
|
|
171
|
+
} else if (element.dataset["assessment"] !== undefined) {
|
|
172
|
+
assess(element, island, component, props, await loadLogic());
|
|
173
|
+
} else {
|
|
174
|
+
const logic = await loadLogic();
|
|
175
|
+
const lang = element.ownerDocument.documentElement.lang || "de";
|
|
176
|
+
const form = element.querySelector<HTMLFormElement>(":scope > form.explainer-component-form");
|
|
177
|
+
const actions = form?.querySelector<HTMLElement>(":scope > .question-actions");
|
|
178
|
+
const check = actions?.querySelector<HTMLButtonElement>(".question-check");
|
|
179
|
+
// hydrates with the task of number 1, as the build rendered it, then shows the task of its own number
|
|
180
|
+
const seeds = JSON.parse(element.dataset["seeds"] ?? "[1]") as number[];
|
|
181
|
+
const count = Math.max(seeds.length, 1);
|
|
182
|
+
const seed = ref(seeds[0] ?? 1);
|
|
183
|
+
// the learner's answer, as the component emits it, and whether "Prüfen" judged this one
|
|
184
|
+
const answer = shallowRef<unknown>(undefined);
|
|
185
|
+
const checked = ref(false);
|
|
186
|
+
const sync = () => {
|
|
187
|
+
if (check) check.disabled = answer.value === undefined || checked.value;
|
|
188
|
+
};
|
|
189
|
+
const onAnswer = (value: unknown) => {
|
|
190
|
+
answer.value = value;
|
|
191
|
+
checked.value = false;
|
|
192
|
+
sync();
|
|
193
|
+
};
|
|
194
|
+
const declaresChecked = element.dataset["checked"] !== undefined;
|
|
195
|
+
createSSRApp({
|
|
196
|
+
render: () =>
|
|
197
|
+
h(component, {
|
|
198
|
+
...props,
|
|
199
|
+
seed: seed.value,
|
|
200
|
+
key: seed.value,
|
|
201
|
+
...(declaresChecked ? { checked: checked.value } : {}),
|
|
202
|
+
onAnswer,
|
|
203
|
+
}),
|
|
204
|
+
}).mount(island);
|
|
205
|
+
form?.addEventListener("submit", (event) => {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
if (answer.value === undefined || checked.value) return;
|
|
208
|
+
const task = logic.generate(seed.value, props);
|
|
209
|
+
const correct = logic.check(task, answer.value) === true;
|
|
210
|
+
const message = correct ? undefined : textOf(logic.reason?.(task, answer.value, props, lang));
|
|
211
|
+
const shown = logic.show === undefined ? undefined : textOf(logic.show(answer.value, task, props, lang));
|
|
212
|
+
checked.value = true;
|
|
213
|
+
sync();
|
|
214
|
+
// after the component has rendered what checked changes, so that the feedback stays in view with the focus
|
|
215
|
+
void nextTick(() => {
|
|
216
|
+
if (feedback) showFeedback(feedback, correct, message);
|
|
217
|
+
});
|
|
218
|
+
element.dispatchEvent(
|
|
219
|
+
new CustomEvent("explainer-answered", {
|
|
220
|
+
bubbles: true,
|
|
221
|
+
detail: {
|
|
222
|
+
correct,
|
|
223
|
+
...(shown === undefined ? {} : { answer: shown }),
|
|
224
|
+
...(message === undefined ? {} : { message }),
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
if (actions) actions.hidden = false;
|
|
230
|
+
sync();
|
|
231
|
+
const id = element.id;
|
|
232
|
+
const from = numberFromAddress(location.href, id);
|
|
233
|
+
let current = from === undefined ? randomTaskNumber(count) : taskNumber(from, count);
|
|
234
|
+
history.replaceState(history.state, "", addressOf(location.href, id, current));
|
|
235
|
+
const controls = element.querySelector<HTMLElement>(":scope > .explainer-task-controls");
|
|
236
|
+
const number = controls?.querySelector<HTMLElement>(".explainer-task-number");
|
|
237
|
+
const show = () => {
|
|
238
|
+
seed.value = seeds[current - 1] ?? current;
|
|
239
|
+
answer.value = undefined;
|
|
240
|
+
checked.value = false;
|
|
241
|
+
sync();
|
|
242
|
+
if (number) number.textContent = (number.dataset["template"] ?? "{n}").replace("{n}", String(current));
|
|
243
|
+
};
|
|
244
|
+
show();
|
|
245
|
+
if (controls) controls.hidden = false;
|
|
246
|
+
controls?.querySelector(".explainer-new-task")?.addEventListener("click", () => {
|
|
247
|
+
current = nextTaskNumber(current, count);
|
|
248
|
+
history.replaceState(history.state, "", addressOf(location.href, id, current));
|
|
249
|
+
show();
|
|
250
|
+
if (feedback) {
|
|
251
|
+
feedback.className = "question-feedback";
|
|
252
|
+
feedback.textContent = "";
|
|
253
|
+
}
|
|
254
|
+
number?.focus();
|
|
255
|
+
element.dispatchEvent(new CustomEvent(SHOWN_EVENT, { bubbles: true }));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
island.hidden = false;
|
|
259
|
+
if (nojs) {
|
|
260
|
+
if (element.dataset["nojs"] === "caption") foldCaption(element, nojs);
|
|
261
|
+
else nojs.hidden = true;
|
|
262
|
+
}
|
|
263
|
+
element.dataset["hydrated"] = "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Hydrates every component of the page. */
|
|
267
|
+
export function startIslands(components: Components, logics: Logics = {}, root: Document = document): void {
|
|
268
|
+
const start = () => {
|
|
269
|
+
for (const element of root.querySelectorAll<HTMLElement>(".explainer-component:not([data-started])")) {
|
|
270
|
+
element.dataset["started"] = "";
|
|
271
|
+
const name = element.dataset["component"] ?? "";
|
|
272
|
+
const load = components[name];
|
|
273
|
+
if (load) void hydrate(element, load, logics[name]);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
start();
|
|
277
|
+
// the components of a self-assessment, once it put them into the page
|
|
278
|
+
if (root.querySelector("template.explainer-assessment-template")) {
|
|
279
|
+
root.addEventListener(ASSESSMENT_UNPACKED_EVENT, start, { once: true });
|
|
280
|
+
}
|
|
281
|
+
}
|