@webwriter/quiz 1.1.3 → 2.0.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/.github/workflows/publish.yml +8 -0
- package/LICENSE +6 -6
- package/README.md +778 -0
- package/assets/icons/grip-horizontal.svg +8 -0
- package/assets/icons/grip-vertical.svg +8 -0
- package/assets/icons/truefalse.svg +6 -0
- package/custom-elements.json +2779 -0
- package/custom.d.ts +180 -176
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +40 -0
- package/dist/migrate.js +1434 -0
- package/dist/widgets/webwriter-choice-item.js +4036 -3895
- package/dist/widgets/webwriter-choice.js +2269 -5821
- package/dist/widgets/webwriter-gap-item.js +3987 -0
- package/dist/widgets/webwriter-gap.js +2908 -0
- package/dist/widgets/webwriter-mark.js +4335 -3489
- package/dist/widgets/webwriter-order-item.js +4014 -3306
- package/dist/widgets/webwriter-order.js +2944 -4386
- package/dist/widgets/webwriter-pairing-item.js +1058 -2421
- package/dist/widgets/webwriter-pairing.js +2993 -4383
- package/dist/widgets/webwriter-quiz-hint.js +2469 -0
- package/dist/widgets/webwriter-quiz.js +4933 -6099
- package/dist/widgets/webwriter-speech.js +6077 -5230
- package/dist/widgets/webwriter-task-prompt.js +4775 -2165
- package/dist/widgets/webwriter-task.js +2364 -8382
- package/dist/widgets/webwriter-text.js +4169 -4337
- package/dist/widgets/webwriter-true-false.js +4674 -0
- package/editing-config.json +106 -0
- package/lit-localize.json +21 -15
- package/localization/de.xlf +407 -74
- package/localization/es.xlf +407 -69
- package/localization/fr.xlf +407 -69
- package/localization/generated/de.ts +112 -29
- package/localization/generated/es.ts +112 -29
- package/localization/generated/fr.ts +112 -29
- package/localization/generated/index.d.ts +5 -0
- package/localization/generated/index.js +2 -2
- package/localization/generated/it.ts +112 -29
- package/localization/generated/locale-codes.js +31 -79
- package/localization/generated/nl.ts +112 -29
- package/localization/it.xlf +407 -69
- package/localization/nl.xlf +407 -69
- package/package.json +119 -252
- package/scripts/apply-translations.mjs +97 -0
- package/src/api/index.ts +72 -0
- package/src/lib/artboard.ts +686 -0
- package/src/lib/draggable-grid.ts +432 -0
- package/src/lib/encrypted-property.ts +202 -0
- package/src/lib/hint-popup.ts +152 -0
- package/src/lib/utils.ts +97 -0
- package/src/lib/webwriter-quirks.ts +121 -0
- package/src/migrate/index.ts +622 -0
- package/src/migrate/legacy-solution.ts +362 -0
- package/src/snippets/order.html +9 -8
- package/src/widgets/choice/webwriter-choice-item.ts +316 -0
- package/src/widgets/choice/webwriter-choice.ts +328 -0
- package/src/widgets/choice/webwriter-true-false.ts +113 -0
- package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
- package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
- package/src/widgets/dnd/webwriter-dnd.ts +36 -0
- package/src/widgets/gap/webwriter-gap-item.ts +250 -0
- package/src/widgets/gap/webwriter-gap.ts +447 -0
- package/src/widgets/order/webwriter-order-item.ts +243 -0
- package/src/widgets/order/webwriter-order.ts +282 -0
- package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
- package/src/widgets/pairing/webwriter-pairing.ts +934 -0
- package/src/widgets/speech/audio-player.ts +325 -0
- package/src/widgets/speech/audio-recorder.ts +285 -0
- package/src/widgets/speech/waveform.ts +113 -0
- package/src/widgets/speech/webwriter-speech.ts +87 -0
- package/src/widgets/webwriter-mark.ts +374 -432
- package/src/widgets/webwriter-quiz-hint.ts +36 -0
- package/src/widgets/webwriter-quiz.ts +320 -282
- package/src/widgets/webwriter-task-prompt.ts +49 -24
- package/src/widgets/webwriter-task.ts +216 -547
- package/src/widgets/webwriter-text.ts +291 -156
- package/tsconfig.api.json +12 -0
- package/tsconfig.json +8 -7
- package/dist/widgets/webwriter-choice.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.js +0 -5866
- package/dist/widgets/webwriter-cloze.css +0 -444
- package/dist/widgets/webwriter-cloze.js +0 -3786
- package/dist/widgets/webwriter-mark.css +0 -444
- package/dist/widgets/webwriter-order.css +0 -444
- package/dist/widgets/webwriter-pairing-item.css +0 -444
- package/dist/widgets/webwriter-pairing.css +0 -444
- package/dist/widgets/webwriter-quiz.css +0 -444
- package/dist/widgets/webwriter-task-explainer.css +0 -444
- package/dist/widgets/webwriter-task-explainer.js +0 -2872
- package/dist/widgets/webwriter-task-hint.css +0 -444
- package/dist/widgets/webwriter-task-hint.js +0 -2811
- package/dist/widgets/webwriter-task.css +0 -444
- package/dist/widgets/webwriter-text.css +0 -444
- package/localization/bg.xlf +0 -69
- package/localization/cs.xlf +0 -69
- package/localization/da.xlf +0 -69
- package/localization/el.xlf +0 -69
- package/localization/et.xlf +0 -69
- package/localization/fi.xlf +0 -69
- package/localization/generated/bg.ts +0 -30
- package/localization/generated/cs.ts +0 -30
- package/localization/generated/da.ts +0 -30
- package/localization/generated/el.ts +0 -30
- package/localization/generated/et.ts +0 -30
- package/localization/generated/fi.ts +0 -30
- package/localization/generated/hu.ts +0 -30
- package/localization/generated/id.ts +0 -30
- package/localization/generated/ja.ts +0 -30
- package/localization/generated/ko.ts +0 -30
- package/localization/generated/lt.ts +0 -30
- package/localization/generated/lv.ts +0 -30
- package/localization/generated/nb.ts +0 -30
- package/localization/generated/pl.ts +0 -30
- package/localization/generated/pt-BR.ts +0 -30
- package/localization/generated/pt-PT.ts +0 -30
- package/localization/generated/ro.ts +0 -30
- package/localization/generated/ru.ts +0 -30
- package/localization/generated/sk.ts +0 -30
- package/localization/generated/sl.ts +0 -30
- package/localization/generated/sv.ts +0 -30
- package/localization/generated/tr.ts +0 -30
- package/localization/generated/uk.ts +0 -30
- package/localization/generated/zh-hans.ts +0 -30
- package/localization/hu.xlf +0 -69
- package/localization/id.xlf +0 -69
- package/localization/ja.xlf +0 -69
- package/localization/ko.xlf +0 -69
- package/localization/lt.xlf +0 -69
- package/localization/lv.xlf +0 -69
- package/localization/nb.xlf +0 -69
- package/localization/pl.xlf +0 -69
- package/localization/pt-BR.xlf +0 -69
- package/localization/pt-PT.xlf +0 -69
- package/localization/ro.xlf +0 -69
- package/localization/ru.xlf +0 -69
- package/localization/sk.xlf +0 -69
- package/localization/sl.xlf +0 -69
- package/localization/sv.xlf +0 -69
- package/localization/tr.xlf +0 -69
- package/localization/uk.xlf +0 -69
- package/localization/zh-hans.xlf +0 -69
- package/src/lib/combobox.ts +0 -456
- package/src/lib/highlighter-fill.svg +0 -6
- package/src/snippets/Beispiel-Choice.html +0 -57
- package/src/snippets/Beispiel-Mark.html +0 -14
- package/src/snippets/Beispiel-Order.html +0 -52
- package/src/snippets/Beispiel-Text.html +0 -20
- package/src/snippets/blank.html +0 -1
- package/src/snippets/choice.html +0 -8
- package/src/snippets/cloze.html +0 -4
- package/src/snippets/mark.html +0 -4
- package/src/snippets/pairing.html +0 -10
- package/src/snippets/speech.html +0 -4
- package/src/snippets/text.html +0 -4
- package/src/snippets/wordsearch.html +0 -4
- package/src/widgets/webwriter-choice-item.ts +0 -254
- package/src/widgets/webwriter-choice.ts +0 -353
- package/src/widgets/webwriter-cloze-gap.ts +0 -216
- package/src/widgets/webwriter-cloze.ts +0 -136
- package/src/widgets/webwriter-order-item.ts +0 -441
- package/src/widgets/webwriter-order.ts +0 -327
- package/src/widgets/webwriter-pairing-item.ts +0 -161
- package/src/widgets/webwriter-pairing.ts +0 -193
- package/src/widgets/webwriter-speech.ts +0 -273
- package/src/widgets/webwriter-task-explainer.ts +0 -43
- package/src/widgets/webwriter-task-hint.ts +0 -23
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import { LitElementWw } from "@webwriter/lit";
|
|
3
|
+
import { css, html } from "lit";
|
|
4
|
+
import { customElement } from "lit/decorators.js";
|
|
5
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
6
|
+
import LOCALIZE from "../../localization/generated";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
"webwriter-quiz-hint": WebwriterQuizHint;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A generic hint element that can be used inside a variety of elements that
|
|
16
|
+
* explicitly mention it in their documentation, such as `<webwriter-task>`.
|
|
17
|
+
*
|
|
18
|
+
* Unless otherwise specified, it should be assigned to the `hint` slot of its
|
|
19
|
+
* parent element.
|
|
20
|
+
*/
|
|
21
|
+
@customElement("webwriter-quiz-hint")
|
|
22
|
+
export class WebwriterQuizHint extends LitElementWw {
|
|
23
|
+
/** @internal */
|
|
24
|
+
localize = LOCALIZE;
|
|
25
|
+
|
|
26
|
+
static styles = css`
|
|
27
|
+
slot {
|
|
28
|
+
display: inline-block !important;
|
|
29
|
+
min-width: 10ch;
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
render() {
|
|
34
|
+
return html`<slot style=${styleMap({ "--ww-placeholder": `"${msg("Hint")}"` })}></slot>`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,282 +1,320 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js";
|
|
3
|
+
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
|
|
4
|
+
import SlDropdown from "@shoelace-style/shoelace/dist/components/dropdown/dropdown.component.js";
|
|
5
|
+
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
|
|
6
|
+
import SlProgressBar from "@shoelace-style/shoelace/dist/components/progress-bar/progress-bar.component.js";
|
|
7
|
+
import { LitElementWw, type OptionDeclaration } from "@webwriter/lit";
|
|
8
|
+
import AddIcon from "bootstrap-icons/icons/plus-lg.svg";
|
|
9
|
+
import { css, html } from "lit";
|
|
10
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
11
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
12
|
+
import { getQuizTypes, onQuizTypesChanged, type IWebWriterQuizType, type QuizType } from "../api";
|
|
13
|
+
import type { WebwriterTask } from "./webwriter-task";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A quiz groups multiple `<webwriter-task>` elements into one exercise that is
|
|
17
|
+
* submitted, graded and reset as a whole.
|
|
18
|
+
*
|
|
19
|
+
* The tasks must be assigned to the default slot.
|
|
20
|
+
*/
|
|
21
|
+
@customElement("webwriter-quiz")
|
|
22
|
+
export class WebwriterQuiz extends LitElementWw {
|
|
23
|
+
/** @internal */
|
|
24
|
+
get dynamicOptions(): Record<string, OptionDeclaration> {
|
|
25
|
+
return {
|
|
26
|
+
"detailed-feedback": { type: "boolean", label: { _: msg("Show detailed feedback") } },
|
|
27
|
+
"confirm-submit": { type: "boolean", label: { _: msg("Require confirmation before submitting") } },
|
|
28
|
+
"confirm-reset": { type: "boolean", label: { _: msg("Require confirmation before resetting") } },
|
|
29
|
+
"hide-points": { type: "boolean", label: { _: msg("Hide points") } },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @internal */
|
|
34
|
+
static scopedElements = {
|
|
35
|
+
"sl-button": SlButton,
|
|
36
|
+
"sl-dialog": SlDialog,
|
|
37
|
+
"sl-icon": SlIcon,
|
|
38
|
+
"sl-dropdown": SlDropdown,
|
|
39
|
+
"sl-progress-bar": SlProgressBar,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
static styles = css`
|
|
43
|
+
:host {
|
|
44
|
+
container-type: inline-size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.header-placeholder {
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
margin-bottom: var(--sl-spacing-x-small);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
::slotted(webwriter-task:not(:last-child)) {
|
|
53
|
+
margin-bottom: var(--sl-spacing-2x-large);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.insert-task-grid {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
59
|
+
gap: var(--sl-spacing-small);
|
|
60
|
+
|
|
61
|
+
sl-button::part(base) {
|
|
62
|
+
justify-content: flex-start;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.add-question {
|
|
67
|
+
margin-top: var(--sl-spacing-small);
|
|
68
|
+
|
|
69
|
+
display: flex;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
|
|
72
|
+
sl-dropdown::part(panel) {
|
|
73
|
+
background-color: var(--sl-color-neutral-0);
|
|
74
|
+
/* Ensure that panel follows the rounded corners of the button */
|
|
75
|
+
border-radius: calc(var(--sl-input-border-radius-medium) + var(--sl-spacing-x-small));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.dropdown-container {
|
|
79
|
+
width: 100cqw;
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
padding: var(--sl-spacing-x-small);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.actions {
|
|
86
|
+
margin-top: 1em;
|
|
87
|
+
display: flex;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.total-result {
|
|
93
|
+
display: flex;
|
|
94
|
+
gap: var(--sl-spacing-small);
|
|
95
|
+
align-items: center;
|
|
96
|
+
|
|
97
|
+
sl-progress-bar {
|
|
98
|
+
width: 300px;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether learners see per-answer feedback and the solution after submitting.
|
|
105
|
+
*/
|
|
106
|
+
@property({ type: Boolean, attribute: "detailed-feedback", reflect: true })
|
|
107
|
+
accessor detailedFeedback: boolean = false;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether learners have to confirm a dialog before submitting.
|
|
111
|
+
*/
|
|
112
|
+
@property({ type: Boolean, attribute: "confirm-submit", reflect: true })
|
|
113
|
+
accessor confirmSubmit: boolean = false;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether learners have to confirm a dialog before resetting.
|
|
117
|
+
*/
|
|
118
|
+
@property({ type: Boolean, attribute: "confirm-reset", reflect: true })
|
|
119
|
+
accessor confirmReset: boolean = false;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Whether the points of each task and the total score are hidden.
|
|
123
|
+
*/
|
|
124
|
+
@property({ type: Boolean, attribute: "hide-points", reflect: true })
|
|
125
|
+
accessor hidePoints: boolean = false;
|
|
126
|
+
|
|
127
|
+
@state() private accessor quizTypes: readonly QuizType[] = getQuizTypes();
|
|
128
|
+
|
|
129
|
+
@state() private accessor totalResult: { score: number; maximum: number } | undefined;
|
|
130
|
+
|
|
131
|
+
@state() private accessor confirmationAction: "submit" | "reset" | undefined;
|
|
132
|
+
|
|
133
|
+
private unsubscribe?: () => void;
|
|
134
|
+
|
|
135
|
+
private addQuestionDropdownRef = createRef<SlDropdown>();
|
|
136
|
+
|
|
137
|
+
connectedCallback() {
|
|
138
|
+
super.connectedCallback();
|
|
139
|
+
this.unsubscribe = onQuizTypesChanged(types => {
|
|
140
|
+
this.quizTypes = [...types];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
disconnectedCallback() {
|
|
145
|
+
super.disconnectedCallback();
|
|
146
|
+
this.unsubscribe?.();
|
|
147
|
+
this.quizTypes = [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected updated(changedProperties: Map<PropertyKey, unknown>) {
|
|
151
|
+
if (changedProperties.has("hidePoints")) {
|
|
152
|
+
this.getTasks().forEach(task => task.requestUpdate?.());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private insertQuizType(quizType: QuizType) {
|
|
157
|
+
const task = document.createElement("webwriter-task");
|
|
158
|
+
|
|
159
|
+
const prompt = document.createElement("webwriter-task-prompt");
|
|
160
|
+
prompt.appendChild(document.createElement("p"));
|
|
161
|
+
task.appendChild(prompt);
|
|
162
|
+
const quiz = quizType.createInstance();
|
|
163
|
+
task.appendChild(quiz);
|
|
164
|
+
this.appendChild(task);
|
|
165
|
+
|
|
166
|
+
this.addQuestionDropdownRef.value?.hide();
|
|
167
|
+
this.requestUpdate();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private InsertQuestionGrid() {
|
|
171
|
+
return html`<div class="insert-task-grid">
|
|
172
|
+
${this.quizTypes.map(
|
|
173
|
+
quizType =>
|
|
174
|
+
html`<sl-button @click=${() => this.insertQuizType(quizType)}>
|
|
175
|
+
<sl-icon src=${quizType.icon ?? ""} slot="prefix"></sl-icon>
|
|
176
|
+
${quizType.getName()}
|
|
177
|
+
</sl-button>`,
|
|
178
|
+
)}
|
|
179
|
+
</div>`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private getTasks() {
|
|
183
|
+
return Array.from(this.children).filter((child): child is WebwriterTask => child.tagName === "WEBWRITER-TASK");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private getTaskApi(task: WebwriterTask): IWebWriterQuizType | undefined {
|
|
187
|
+
return Array.from(task.children).find(
|
|
188
|
+
(child): child is HTMLElement & IWebWriterQuizType =>
|
|
189
|
+
typeof (child as Partial<IWebWriterQuizType>).checkValidity === "function",
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getTaskPoints(task: WebwriterTask) {
|
|
194
|
+
const points = Number(task.getAttribute("points") ?? 1);
|
|
195
|
+
return Number.isFinite(points) && points > 0 ? points : 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private formatScore(score: number) {
|
|
199
|
+
return Number(score.toFixed(2));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private getResultPercentage(result: { score: number; maximum: number }) {
|
|
203
|
+
if (result.maximum <= 0) return 0;
|
|
204
|
+
return (result.score / result.maximum) * 100;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private resetAnswers() {
|
|
208
|
+
for (const task of this.getTasks()) {
|
|
209
|
+
this.getTaskApi(task)?.reset();
|
|
210
|
+
task.resetFeedback();
|
|
211
|
+
}
|
|
212
|
+
this.totalResult = undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private submit() {
|
|
216
|
+
const tasksWithApi = this.getTasks().map(task => ({
|
|
217
|
+
task,
|
|
218
|
+
api: this.getTaskApi(task),
|
|
219
|
+
}));
|
|
220
|
+
const validities = tasksWithApi.map(({ task, api }) => {
|
|
221
|
+
const valid = api?.checkValidity() ?? false;
|
|
222
|
+
task.showRequiredWarning(!valid);
|
|
223
|
+
task.showScore(undefined);
|
|
224
|
+
return valid;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
this.totalResult = undefined;
|
|
228
|
+
if (validities.some(valid => !valid)) return;
|
|
229
|
+
|
|
230
|
+
let score = 0;
|
|
231
|
+
let maximum = 0;
|
|
232
|
+
for (const { task, api } of tasksWithApi) {
|
|
233
|
+
const answerScore = api?.checkAnswer?.(this.detailedFeedback);
|
|
234
|
+
if (answerScore === undefined) continue;
|
|
235
|
+
|
|
236
|
+
const points = this.getTaskPoints(task);
|
|
237
|
+
const taskScore = Math.max(0, Math.min(1, answerScore)) * points;
|
|
238
|
+
task.showScore(taskScore);
|
|
239
|
+
score += taskScore;
|
|
240
|
+
maximum += points;
|
|
241
|
+
}
|
|
242
|
+
this.totalResult = { score, maximum };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private ConfirmationDialog() {
|
|
246
|
+
const isSubmit = this.confirmationAction === "submit";
|
|
247
|
+
|
|
248
|
+
const cancel = () => (this.confirmationAction = undefined);
|
|
249
|
+
const confirm = () => {
|
|
250
|
+
const action = this.confirmationAction;
|
|
251
|
+
this.confirmationAction = undefined;
|
|
252
|
+
if (action === "submit") this.submit();
|
|
253
|
+
else if (action === "reset") this.resetAnswers();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return html`<sl-dialog
|
|
257
|
+
label=${isSubmit ? msg("Submit answers?") : msg("Reset answers?")}
|
|
258
|
+
?open=${this.confirmationAction !== undefined}
|
|
259
|
+
@sl-request-close=${cancel}
|
|
260
|
+
@sl-after-hide=${cancel}
|
|
261
|
+
>
|
|
262
|
+
${isSubmit ? msg("Submit your answers for evaluation?") : msg("Reset all answers and start over?")}
|
|
263
|
+
<sl-button slot="footer" @click=${cancel}>${msg("Cancel")}</sl-button>
|
|
264
|
+
<sl-button slot="footer" variant=${isSubmit ? "primary" : "danger"} @click=${confirm}>
|
|
265
|
+
${isSubmit ? msg("Submit") : msg("Reset answers")}
|
|
266
|
+
</sl-button>
|
|
267
|
+
</sl-dialog>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
render() {
|
|
271
|
+
if (!this.isContentEditable) {
|
|
272
|
+
return html`<slot></slot>
|
|
273
|
+
|
|
274
|
+
<div class="actions">
|
|
275
|
+
${this.totalResult
|
|
276
|
+
? html`<div class="total-result">
|
|
277
|
+
<sl-progress-bar .value=${this.getResultPercentage(this.totalResult)}></sl-progress-bar>
|
|
278
|
+
<strong
|
|
279
|
+
>${this.formatScore(this.totalResult.score)} / ${this.formatScore(this.totalResult.maximum)}</strong
|
|
280
|
+
>
|
|
281
|
+
</div>`
|
|
282
|
+
: html`<sl-button
|
|
283
|
+
variant="primary"
|
|
284
|
+
@click=${() => (this.confirmSubmit ? (this.confirmationAction = "submit") : this.submit())}
|
|
285
|
+
>
|
|
286
|
+
${msg("Submit")}
|
|
287
|
+
</sl-button>`}
|
|
288
|
+
<sl-button @click=${() => (this.confirmReset ? (this.confirmationAction = "reset") : this.resetAnswers())}>
|
|
289
|
+
${msg("Reset answers")}
|
|
290
|
+
</sl-button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
${this.ConfirmationDialog()}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.children.length > 0) {
|
|
297
|
+
return html`<slot
|
|
298
|
+
@slotchange=${() => {
|
|
299
|
+
// Update numbering in the quiz header in case the order changed
|
|
300
|
+
Array.from(this.children)
|
|
301
|
+
.filter(c => c.tagName === "WEBWRITER-TASK")
|
|
302
|
+
.forEach(t => (t as WebwriterTask).requestUpdate?.());
|
|
303
|
+
this.requestUpdate();
|
|
304
|
+
}}
|
|
305
|
+
></slot>
|
|
306
|
+
<div class="add-question">
|
|
307
|
+
<sl-dropdown placement="bottom" distance="4" ${ref(this.addQuestionDropdownRef)}>
|
|
308
|
+
<sl-button slot="trigger" caret>
|
|
309
|
+
<sl-icon slot="prefix" src=${AddIcon}></sl-icon>
|
|
310
|
+
${msg("Add Question")}
|
|
311
|
+
</sl-button>
|
|
312
|
+
<div class="dropdown-container">${this.InsertQuestionGrid()}</div>
|
|
313
|
+
</sl-dropdown>
|
|
314
|
+
</div>`;
|
|
315
|
+
} else {
|
|
316
|
+
return html`<div class="header-placeholder">${msg("Question 1")}</div>
|
|
317
|
+
${this.InsertQuestionGrid()}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|