@webwriter/quiz 1.1.5 → 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/LICENSE +1 -1
- 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 +5 -1
- 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 +4095 -3818
- package/dist/widgets/webwriter-choice.js +2179 -5598
- package/dist/widgets/webwriter-gap-item.js +3987 -0
- package/dist/widgets/webwriter-gap.js +2908 -0
- package/dist/widgets/webwriter-mark.js +4347 -3368
- package/dist/widgets/webwriter-order-item.js +4038 -3197
- package/dist/widgets/webwriter-order.js +2885 -4200
- package/dist/widgets/webwriter-pairing-item.js +1070 -2330
- package/dist/widgets/webwriter-pairing.js +2968 -4231
- package/dist/widgets/webwriter-quiz-hint.js +2469 -0
- package/dist/widgets/webwriter-quiz.js +3337 -4364
- package/dist/widgets/webwriter-speech.js +6100 -5120
- package/dist/widgets/webwriter-task-prompt.js +4787 -2068
- package/dist/widgets/webwriter-task.js +2264 -8157
- package/dist/widgets/webwriter-text.js +4075 -4132
- package/dist/widgets/webwriter-true-false.js +4674 -0
- package/editing-config.json +106 -0
- package/lit-localize.json +20 -14
- package/localization/de.xlf +395 -65
- package/localization/es.xlf +396 -61
- package/localization/fr.xlf +396 -61
- package/localization/generated/de.ts +97 -15
- package/localization/generated/es.ts +97 -15
- package/localization/generated/fr.ts +98 -16
- package/localization/generated/index.d.ts +5 -0
- package/localization/generated/it.ts +96 -14
- package/localization/generated/locale-codes.js +0 -48
- package/localization/generated/nl.ts +99 -17
- package/localization/it.xlf +396 -61
- package/localization/nl.xlf +396 -61
- package/package.json +120 -254
- 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 +8 -7
- 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 +366 -424
- package/src/widgets/webwriter-quiz-hint.ts +36 -0
- package/src/widgets/webwriter-quiz.ts +318 -280
- package/src/widgets/webwriter-task-prompt.ts +43 -18
- package/src/widgets/webwriter-task.ts +212 -551
- package/src/widgets/webwriter-text.ts +286 -172
- package/tsconfig.api.json +12 -0
- package/tsconfig.json +7 -6
- 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 -5727
- package/dist/widgets/webwriter-cloze.css +0 -444
- package/dist/widgets/webwriter-cloze.js +0 -3653
- 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 -2769
- package/dist/widgets/webwriter-task-hint.css +0 -444
- package/dist/widgets/webwriter-task-hint.js +0 -2708
- package/dist/widgets/webwriter-task.css +0 -444
- package/dist/widgets/webwriter-text.css +0 -444
- package/localization/bg.xlf +0 -72
- package/localization/cs.xlf +0 -72
- package/localization/da.xlf +0 -72
- package/localization/el.xlf +0 -72
- package/localization/et.xlf +0 -72
- package/localization/fi.xlf +0 -72
- package/localization/generated/bg.ts +0 -31
- package/localization/generated/cs.ts +0 -31
- package/localization/generated/da.ts +0 -31
- package/localization/generated/el.ts +0 -31
- package/localization/generated/et.ts +0 -31
- package/localization/generated/fi.ts +0 -31
- package/localization/generated/hu.ts +0 -31
- package/localization/generated/id.ts +0 -31
- package/localization/generated/ja.ts +0 -31
- package/localization/generated/ko.ts +0 -31
- package/localization/generated/lt.ts +0 -31
- package/localization/generated/lv.ts +0 -31
- package/localization/generated/nb.ts +0 -31
- package/localization/generated/pl.ts +0 -31
- package/localization/generated/pt-BR.ts +0 -31
- package/localization/generated/pt-PT.ts +0 -31
- package/localization/generated/ro.ts +0 -31
- package/localization/generated/ru.ts +0 -31
- package/localization/generated/sk.ts +0 -31
- package/localization/generated/sl.ts +0 -31
- package/localization/generated/sv.ts +0 -31
- package/localization/generated/tr.ts +0 -31
- package/localization/generated/uk.ts +0 -31
- package/localization/generated/zh-hans.ts +0 -31
- package/localization/hu.xlf +0 -72
- package/localization/id.xlf +0 -72
- package/localization/ja.xlf +0 -72
- package/localization/ko.xlf +0 -72
- package/localization/lt.xlf +0 -72
- package/localization/lv.xlf +0 -72
- package/localization/nb.xlf +0 -72
- package/localization/pl.xlf +0 -72
- package/localization/pt-BR.xlf +0 -72
- package/localization/pt-PT.xlf +0 -72
- package/localization/ro.xlf +0 -72
- package/localization/ru.xlf +0 -72
- package/localization/sk.xlf +0 -72
- package/localization/sl.xlf +0 -72
- package/localization/sv.xlf +0 -72
- package/localization/tr.xlf +0 -72
- package/localization/uk.xlf +0 -72
- package/localization/zh-hans.xlf +0 -72
- 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 -257
- 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
|
@@ -1,555 +1,216 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import { WebwriterTaskExplainer } from "./webwriter-task-explainer"
|
|
23
|
-
|
|
24
|
-
import LOCALIZE from "../../localization/generated"
|
|
25
|
-
import {msg} from "@lit/localize"
|
|
26
|
-
|
|
27
|
-
async function arrayBufferToDataUrl(buffer: ArrayBuffer | Uint8Array) {
|
|
28
|
-
return new Promise(r => {
|
|
29
|
-
const reader = new FileReader()
|
|
30
|
-
reader.onload = () => r(reader.result as string)
|
|
31
|
-
reader.readAsDataURL(new Blob([buffer]))
|
|
32
|
-
}) as Promise<string>
|
|
1
|
+
import { msg, str } from "@lit/localize";
|
|
2
|
+
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js";
|
|
3
|
+
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
|
|
4
|
+
import SlTag from "@shoelace-style/shoelace/dist/components/tag/tag.component.js";
|
|
5
|
+
import { LitElementWw, type OptionDeclaration } from "@webwriter/lit";
|
|
6
|
+
import ArrowDownIcon from "bootstrap-icons/icons/arrow-down.svg";
|
|
7
|
+
import ArrowUpIcon from "bootstrap-icons/icons/arrow-up.svg";
|
|
8
|
+
import WarningIcon from "bootstrap-icons/icons/exclamation-triangle.svg";
|
|
9
|
+
import TrashIcon from "bootstrap-icons/icons/trash.svg";
|
|
10
|
+
import { css, html, nothing } from "lit";
|
|
11
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
12
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
13
|
+
import type { IWebWriterQuizType } from "../api";
|
|
14
|
+
import { commonStyles } from "../lib/utils";
|
|
15
|
+
import { mutateWithStableSelection } from "../lib/webwriter-quirks";
|
|
16
|
+
import type { WebwriterQuiz } from "./webwriter-quiz";
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
"webwriter-task": WebwriterTask;
|
|
21
|
+
}
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{name: "PBKDF2"},
|
|
45
|
-
false,
|
|
46
|
-
["deriveBits", "deriveKey"]
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getKey(keyMaterial: CryptoKey, salt: ArrayBufferView) {
|
|
51
|
-
return window.crypto.subtle.deriveKey(
|
|
52
|
-
{
|
|
53
|
-
"name": "PBKDF2",
|
|
54
|
-
salt: salt,
|
|
55
|
-
"iterations": 100000,
|
|
56
|
-
"hash": "SHA-256"
|
|
57
|
-
},
|
|
58
|
-
keyMaterial,
|
|
59
|
-
{ "name": "AES-GCM", "length": 256},
|
|
60
|
-
true,
|
|
61
|
-
[ "encrypt", "decrypt" ]
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function romanOrdinal(num: number, capitalize=false) {
|
|
66
|
-
let roman = {
|
|
67
|
-
m: 1000,
|
|
68
|
-
cm: 900,
|
|
69
|
-
d: 500,
|
|
70
|
-
cd: 400,
|
|
71
|
-
c: 100,
|
|
72
|
-
xc: 90,
|
|
73
|
-
l: 50,
|
|
74
|
-
xl: 40,
|
|
75
|
-
x: 10,
|
|
76
|
-
ix: 9,
|
|
77
|
-
v: 5,
|
|
78
|
-
iv: 4,
|
|
79
|
-
i: 1
|
|
80
|
-
};
|
|
81
|
-
let str = ''
|
|
82
|
-
|
|
83
|
-
for (let i of Object.keys(roman)) {
|
|
84
|
-
let q = Math.floor(num / roman[i])
|
|
85
|
-
num -= q * roman[i]
|
|
86
|
-
str += i.repeat(q)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return capitalize? str.toUpperCase(): str
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function alphabeticalOrdinal(num: number, capitalize=false, alphabet="abcdefghijklmnopqrstuvwxyz") {
|
|
93
|
-
const a = alphabet[0]
|
|
94
|
-
const k = alphabet.length
|
|
95
|
-
const str = a.repeat(Math.floor(num / k)) + alphabet[num % k]
|
|
96
|
-
return capitalize? str.toUpperCase(): str
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
interface Answer {
|
|
102
|
-
solution: any
|
|
103
|
-
reportSolution(): void
|
|
104
|
-
reset(): void
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
declare global {interface HTMLElementTagNameMap {
|
|
108
|
-
"webwriter-task": WebwriterTask;
|
|
109
|
-
}}
|
|
110
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A task describes a single quiz question, which consists of a
|
|
26
|
+
* `<webwriter-task-prompt>`, an optional `<webwriter-quiz-hint>` and an answer
|
|
27
|
+
* of a certain type, such as `<webwriter-choice>`.
|
|
28
|
+
*
|
|
29
|
+
* The `<webwriter-task-prompt>` must be assigned to the `prompt` slot, the
|
|
30
|
+
* `<webwriter-quiz-hint>` must be assigned to the `hint` slot and the answer
|
|
31
|
+
* must be assigned to the default slot.
|
|
32
|
+
*/
|
|
111
33
|
@customElement("webwriter-task")
|
|
112
34
|
export class WebwriterTask extends LitElementWw {
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.hint = false
|
|
296
|
-
this.hintSlotEl.assignedElements().forEach(el => el.remove())
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
get explainers(): WebwriterTaskExplainer[] {
|
|
302
|
-
return Array.from(this.querySelectorAll("webwriter-task-explainer")) as unknown as WebwriterTaskExplainer[]
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
toggleExplainers = () => {
|
|
306
|
-
if(this.explainers.length) {
|
|
307
|
-
this.explainers.forEach(explainer => explainer.remove())
|
|
308
|
-
this.activeExplainer = undefined
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
const solutionExplainer = this.ownerDocument.createElement("webwriter-task-explainer")
|
|
312
|
-
solutionExplainer.slot = "explainer"
|
|
313
|
-
solutionExplainer.id = "solution"
|
|
314
|
-
solutionExplainer.active = true/*
|
|
315
|
-
const elseExplainer = this.ownerDocument.createElement("webwriter-task-explainer")
|
|
316
|
-
elseExplainer.slot = "explainer"
|
|
317
|
-
elseExplainer.id = "else"*/
|
|
318
|
-
this.append(solutionExplainer)
|
|
319
|
-
this.ownerDocument.getSelection().setBaseAndExtent(solutionExplainer, 0, solutionExplainer, 0)
|
|
320
|
-
this.requestUpdate()
|
|
321
|
-
this.activeExplainer = "solution"
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
@property({attribute: false, state: true})
|
|
326
|
-
private accessor hintOpen = false
|
|
327
|
-
|
|
328
|
-
@property({attribute: true, reflect: true})
|
|
329
|
-
accessor counter: "number" | "roman" | "roman-capitalized" | "alphabetical" | "alphabetical-capitalized"
|
|
330
|
-
|
|
331
|
-
get index() {
|
|
332
|
-
return [...(this?.parentElement?.children ?? [])].indexOf(this)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
get ordinalExpr() {
|
|
336
|
-
if(this.index === undefined || this.index === -1) {
|
|
337
|
-
return undefined
|
|
338
|
-
}
|
|
339
|
-
if(this.counter === "number") {
|
|
340
|
-
return `${this.index + 1}.`
|
|
341
|
-
}
|
|
342
|
-
else if(this.counter === "roman") {
|
|
343
|
-
return `${romanOrdinal(this.index + 1)}.`
|
|
344
|
-
}
|
|
345
|
-
else if(this.counter === "roman-capitalized") {
|
|
346
|
-
return `${romanOrdinal(this.index + 1, true)}.`
|
|
347
|
-
}
|
|
348
|
-
else if(this.counter === "alphabetical") {
|
|
349
|
-
return `${alphabeticalOrdinal(this.index)}.`
|
|
350
|
-
}
|
|
351
|
-
else if(this.counter === "alphabetical-capitalized") {
|
|
352
|
-
return `${alphabeticalOrdinal(this.index, true)}.`
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
observer: MutationObserver
|
|
357
|
-
|
|
358
|
-
connectedCallback(): void {
|
|
359
|
-
super.connectedCallback()
|
|
360
|
-
this.observer = new MutationObserver(() => this.requestUpdate())
|
|
361
|
-
this.parentElement && this.observer.observe(this.parentElement, {childList: true})
|
|
362
|
-
this.addEventListener("keydown", (e) => this.handleHintKeydown(e))
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
protected firstUpdated(_changedProperties: PropertyValues): void {
|
|
366
|
-
if(this.isContentEditable) {
|
|
367
|
-
this.#decodeSolution()
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
disconnectedCallback(): void {
|
|
373
|
-
super.disconnectedCallback()
|
|
374
|
-
this.observer.disconnect()
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
@property({type: String, attribute: true, reflect: true})
|
|
378
|
-
accessor solution: string
|
|
379
|
-
|
|
380
|
-
/** Property containing the password currently entered by the author or user */
|
|
381
|
-
@property({type: String, attribute: false, reflect: false})
|
|
382
|
-
// @option({type: String, label: {_: "Password"}, description: {_: "Password-protects quiz answers"}})
|
|
383
|
-
accessor password: string = "B08bxd82SAOf"
|
|
384
|
-
|
|
385
|
-
@property({type: String, attribute: true, reflect: true})
|
|
386
|
-
accessor salt: string
|
|
387
|
-
|
|
388
|
-
@property({type: String, attribute: true, reflect: true})
|
|
389
|
-
accessor iv: string
|
|
390
|
-
|
|
391
|
-
async #encodeSolution() {
|
|
392
|
-
const value = this.answer.solution as any
|
|
393
|
-
let keyMaterial = await getKeyMaterial(this.password)
|
|
394
|
-
let salt = window.crypto.getRandomValues(new Uint8Array(16))
|
|
395
|
-
let iv = window.crypto.getRandomValues(new Uint8Array(12))
|
|
396
|
-
let key = await getKey(keyMaterial, salt)
|
|
397
|
-
let encoder = new TextEncoder();
|
|
398
|
-
let encodedMessage = encoder.encode(JSON.stringify(value))
|
|
399
|
-
const solution = await window.crypto.subtle.encrypt(
|
|
400
|
-
{name: "AES-GCM", iv},
|
|
401
|
-
key,
|
|
402
|
-
encodedMessage
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
this.solution = await arrayBufferToDataUrl(solution)
|
|
406
|
-
this.iv = await arrayBufferToDataUrl(iv)
|
|
407
|
-
this.salt = await arrayBufferToDataUrl(salt)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
async #decodeSolution() {
|
|
411
|
-
if(!this.solution) {
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
const encodedSolution = await dataUrlToArrayBuffer(this.solution)
|
|
415
|
-
const iv = await dataUrlToArrayBuffer(this.iv)
|
|
416
|
-
const salt = await dataUrlToArrayBuffer(this.salt)
|
|
417
|
-
let keyMaterial = await getKeyMaterial(this.password)
|
|
418
|
-
let key = await getKey(keyMaterial, new Uint8Array(salt))
|
|
419
|
-
try {
|
|
420
|
-
const solutionBuffer = await window.crypto.subtle.decrypt({name: "AES-GCM", iv}, key, encodedSolution)
|
|
421
|
-
let decoder = new TextDecoder()
|
|
422
|
-
const solution = JSON.parse(decoder.decode(solutionBuffer))
|
|
423
|
-
this.answer.solution = solution
|
|
424
|
-
}
|
|
425
|
-
catch(err) {
|
|
426
|
-
console.error(err)
|
|
427
|
-
throw new Error("Invalid password")
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
checkSolution() {
|
|
432
|
-
const solution = this.#decodeSolution()
|
|
433
|
-
return Object.entries(solution).every(([k, v]) => {
|
|
434
|
-
return JSON.stringify(this.answer[k]) === JSON.stringify(v)
|
|
435
|
-
})
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
reportSolution() {
|
|
439
|
-
// @ts-ignore
|
|
440
|
-
this.answer.reportSolution(this.#decodeSolution())
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
@query("slot:not([name])")
|
|
444
|
-
accessor slotEl: HTMLSlotElement
|
|
445
|
-
|
|
446
|
-
@query("slot[name=hint]")
|
|
447
|
-
accessor hintSlotEl: HTMLSlotElement
|
|
448
|
-
|
|
449
|
-
get answer() {
|
|
450
|
-
return this.slotEl?.assignedElements()[0] as HTMLElement
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
handleHintSlotChange = (e: Event) => {
|
|
456
|
-
if(!this.hasHintElement) {
|
|
457
|
-
this.hintOpen = false
|
|
458
|
-
this.hint = false
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
handleHintKeydown = (e: KeyboardEvent) => {
|
|
463
|
-
// console.log(document.getSelection().anchorOffset === 0, this.hints.includes(document.getSelection().anchorNode.parentElement))
|
|
464
|
-
if(e.key === "Backspace" && document.getSelection().anchorOffset === 0 && this.hints.includes(document.getSelection().anchorNode.parentElement)) {
|
|
465
|
-
this.hintOpen = false
|
|
466
|
-
this.hint = false
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
handleSubmit = async () => {
|
|
471
|
-
if(this.answer.freeText === true){
|
|
472
|
-
let submitButton: SlButton = this.shadowRoot.getElementById("submit") as SlButton
|
|
473
|
-
submitButton.style.visibility = "hidden"
|
|
474
|
-
}
|
|
475
|
-
await this.#decodeSolution()
|
|
476
|
-
this.answer.reportSolution()
|
|
477
|
-
this.dispatchEvent(new SubmitEvent("submit", {bubbles: true, composed: true}))
|
|
478
|
-
this.activeExplainer = this.explainers[0].id
|
|
479
|
-
this.submitted = true
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
handleReset = () => {
|
|
483
|
-
if(this.answer.freeText === true){
|
|
484
|
-
let submitButton: SlButton = this.shadowRoot.getElementById("submit") as SlButton
|
|
485
|
-
submitButton.style.visibility = "visible"
|
|
486
|
-
}
|
|
487
|
-
this.answer.reset && this.answer.reset()
|
|
488
|
-
this.isChanged = this.answer.freeText === true ? this.isChanged : false
|
|
489
|
-
this.submitted = false
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
handleAnswerChange = async (e: CustomEvent) => {
|
|
493
|
-
this.isChanged = true
|
|
494
|
-
if(this.isContentEditable) {
|
|
495
|
-
this.#encodeSolution()
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
handleSlotChange = (e: Event) => {
|
|
500
|
-
this.requestUpdate()
|
|
501
|
-
if(this.isContentEditable) {
|
|
502
|
-
const solution = this.#decodeSolution() ?? {}
|
|
503
|
-
Object.entries(solution).forEach(([k, v]) => {
|
|
504
|
-
this.answer[k] = v
|
|
505
|
-
})
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
@property()
|
|
510
|
-
accessor activeExplainer: string
|
|
511
|
-
|
|
512
|
-
selectExplainer(id: string) {
|
|
513
|
-
const explainer = this.explainers.find(node => node.id === id)
|
|
514
|
-
explainer.active = true
|
|
515
|
-
this.explainers.filter(node => node.id !== id).forEach(node => node.active = false)
|
|
516
|
-
this.activeExplainer = id
|
|
517
|
-
setTimeout(() => this.ownerDocument.getSelection().setBaseAndExtent(explainer, 0, explainer, 0))
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
get explainerLabels() {
|
|
521
|
-
return {
|
|
522
|
-
"solution": "Explainer",
|
|
523
|
-
"else": "Else"
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
render() {
|
|
528
|
-
return html`
|
|
529
|
-
<header>
|
|
530
|
-
<span>${this.ordinalExpr}</span>
|
|
531
|
-
<slot name="prompt"></slot>
|
|
532
|
-
<div id="task-buttons">
|
|
533
|
-
<sl-icon-button style="display: none" class="author-only" id="feedback" src=${!this.explainers.length? IconPatchCheck: IconPatchCheckFill} @click=${() => this.toggleExplainers()}></sl-icon-button>
|
|
534
|
-
<sl-popup id="hint-popup" ?active=${this.hintOpen} placement="left" arrow auto-size shift @selectstart=${e => e.stopImmediatePropagation()}>
|
|
535
|
-
<sl-icon-button slot="anchor" id="hint" src=${!this.hasHintContent && !this.hintOpen? IconPatchQuestion: IconPatchQuestionFill} @click=${() => this.toggleHint()}></sl-icon-button>
|
|
536
|
-
<div id="hint-content">
|
|
537
|
-
<slot name="hint" @slotchange=${this.handleHintSlotChange}></slot>
|
|
538
|
-
</div>
|
|
539
|
-
</sl-popup>
|
|
540
|
-
</div>
|
|
541
|
-
</header>
|
|
542
|
-
<slot @ww-answer-change=${this.handleAnswerChange} @slotchange=${this.handleSlotChange} ?inert=${this.submitted}></slot>
|
|
543
|
-
<sl-tab-group id="explainer-group" placement="end" ?data-empty=${!this.explainers.length} ?data-single=${this.explainers.length === 1}>
|
|
544
|
-
${this.explainers.map((explainer, i) => html`<sl-tab ?active=${this.activeExplainer === explainer.id} slot="nav" @click=${() => this.selectExplainer(explainer.id)}>${this.explainerLabels[explainer.id] ?? explainer.id}</sl-tab>`)}
|
|
545
|
-
<slot name="explainer" style=${styleMap({"--ww-placeholder": `"${msg("Explanation")}"`})}></slot>
|
|
546
|
-
</sl-tab-group>
|
|
547
|
-
${!this.directSubmit || !this.answer?.reportSolution? null: html`
|
|
548
|
-
<sl-button-group class="user-only user-actions">
|
|
549
|
-
<sl-button id="submit" @click=${this.handleSubmit}>${this.answer.freeText != true ? msg("Check your answers") : msg("Save answer")}</sl-button>
|
|
550
|
-
<sl-button ?disabled=${!this.isChanged && !this.submitted} id="reset" class="user-only" @click=${this.handleReset}>${this.answer.freeText != true ? msg("Try again") : msg("Reset")}</sl-button>
|
|
551
|
-
</sl-button-group>
|
|
552
|
-
`}
|
|
553
|
-
`
|
|
554
|
-
}
|
|
555
|
-
}
|
|
35
|
+
/** @internal */
|
|
36
|
+
get dynamicOptions(): Record<string, OptionDeclaration> {
|
|
37
|
+
return {
|
|
38
|
+
"points": { type: Number, label: { _: msg("Points") } },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @internal */
|
|
43
|
+
static scopedElements = {
|
|
44
|
+
"sl-icon-button": SlIconButton,
|
|
45
|
+
"sl-icon": SlIcon,
|
|
46
|
+
"sl-tag": SlTag,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
static styles = [
|
|
50
|
+
commonStyles,
|
|
51
|
+
css`
|
|
52
|
+
:host {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
sl-tag {
|
|
57
|
+
margin-top: -5px;
|
|
58
|
+
margin-bottom: -5px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.quiz-header {
|
|
62
|
+
margin-bottom: var(--sl-spacing-3x-small);
|
|
63
|
+
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
|
|
67
|
+
.title {
|
|
68
|
+
font-weight: bold;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.required-warning {
|
|
72
|
+
margin-left: var(--sl-spacing-small);
|
|
73
|
+
|
|
74
|
+
sl-icon {
|
|
75
|
+
padding-right: var(--sl-spacing-2x-small);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.spacer {
|
|
80
|
+
flex-grow: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.actions {
|
|
84
|
+
opacity: 0;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
|
|
88
|
+
sl-icon-button::part(base) {
|
|
89
|
+
/* Ensures that icon buttons match the 1.5 line height of the header text */
|
|
90
|
+
padding: 0.25em;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.points {
|
|
95
|
+
font-variant-numeric: tabular-nums;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
:host(:hover) .actions {
|
|
100
|
+
opacity: 1;
|
|
101
|
+
}
|
|
102
|
+
`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
@property({ type: Number, attribute: "points", reflect: true })
|
|
106
|
+
private accessor points = 1;
|
|
107
|
+
|
|
108
|
+
@state() private accessor requiredWarning = false;
|
|
109
|
+
|
|
110
|
+
@state() private accessor score: number | undefined;
|
|
111
|
+
|
|
112
|
+
@state() private accessor gradable = false;
|
|
113
|
+
|
|
114
|
+
private gradabilityUpdate = 0;
|
|
115
|
+
|
|
116
|
+
connectedCallback() {
|
|
117
|
+
super.connectedCallback();
|
|
118
|
+
this.updateGradability();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected willUpdate(changedProperties: Map<PropertyKey, unknown>) {
|
|
122
|
+
if (changedProperties.has("points") && (!Number.isFinite(this.points) || this.points <= 0)) {
|
|
123
|
+
this.points = 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @internal */
|
|
128
|
+
showRequiredWarning(show: boolean) {
|
|
129
|
+
this.requiredWarning = show;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @internal */
|
|
133
|
+
showScore(score: number | undefined) {
|
|
134
|
+
this.score = score;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @internal */
|
|
138
|
+
resetFeedback() {
|
|
139
|
+
this.requiredWarning = false;
|
|
140
|
+
this.score = undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private formatScore(score: number) {
|
|
144
|
+
return Number(score.toFixed(2));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private updateGradability() {
|
|
148
|
+
const update = ++this.gradabilityUpdate;
|
|
149
|
+
const questionTypes = Array.from(this.children).filter(child => !child.slot);
|
|
150
|
+
this.gradable = questionTypes.some(
|
|
151
|
+
child => typeof (child as Partial<IWebWriterQuizType>).checkAnswer === "function",
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
for (const questionType of questionTypes) {
|
|
155
|
+
if (customElements.get(questionType.localName)) continue;
|
|
156
|
+
customElements.whenDefined(questionType.localName).then(() => {
|
|
157
|
+
if (update === this.gradabilityUpdate) this.updateGradability();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private QuizHeader() {
|
|
163
|
+
const quiz = this.parentElement as WebwriterQuiz | null;
|
|
164
|
+
if (!quiz || quiz.tagName !== "WEBWRITER-QUIZ") return nothing;
|
|
165
|
+
|
|
166
|
+
const allTasks = Array.from(quiz.children);
|
|
167
|
+
const index = allTasks.indexOf(this);
|
|
168
|
+
|
|
169
|
+
return html`
|
|
170
|
+
<div class=${classMap({ "quiz-header": true })}>
|
|
171
|
+
<div class="title">${msg(str`Question ${index + 1}`)}</div>
|
|
172
|
+
${this.requiredWarning
|
|
173
|
+
? html`<sl-tag variant="danger" class="required-warning">
|
|
174
|
+
<sl-icon src=${WarningIcon}></sl-icon> ${msg("Unanswered")}
|
|
175
|
+
</sl-tag>`
|
|
176
|
+
: nothing}
|
|
177
|
+
<div class="spacer"></div>
|
|
178
|
+
<span class="actions author-only">
|
|
179
|
+
<sl-icon-button
|
|
180
|
+
src=${ArrowUpIcon}
|
|
181
|
+
?disabled=${index === 0}
|
|
182
|
+
@click=${() => mutateWithStableSelection(this, () => allTasks[index - 1].before(this))}
|
|
183
|
+
></sl-icon-button>
|
|
184
|
+
<sl-icon-button
|
|
185
|
+
src=${ArrowDownIcon}
|
|
186
|
+
?disabled=${index === allTasks.length - 1}
|
|
187
|
+
@click=${() => mutateWithStableSelection(this, () => allTasks[index + 1].after(this))}
|
|
188
|
+
></sl-icon-button>
|
|
189
|
+
<sl-icon-button src=${TrashIcon} @click=${() => this.remove()}></sl-icon-button>
|
|
190
|
+
</span>
|
|
191
|
+
${this.gradable ? this.TaskResult(quiz) : nothing}
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private TaskResult(quiz: WebwriterQuiz) {
|
|
197
|
+
if (quiz.hidePoints) {
|
|
198
|
+
if (this.score === undefined) return nothing;
|
|
199
|
+
if (this.score === 0) return html`<sl-tag variant="danger">${msg("Incorrect")}</sl-tag>`;
|
|
200
|
+
if (this.score === this.points) return html`<sl-tag variant="success">${msg("Correct")}</sl-tag>`;
|
|
201
|
+
return html`<sl-tag variant="warning">${msg("Partially correct")}</sl-tag>`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return html`<span>
|
|
205
|
+
${this.score !== undefined ? html`${this.formatScore(this.score)} / ` : nothing}
|
|
206
|
+
<span class="points">${this.points}</span>
|
|
207
|
+
${this.points === 1 ? msg("point") : msg("points")}
|
|
208
|
+
</span>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
render() {
|
|
212
|
+
return html`${this.QuizHeader()}
|
|
213
|
+
<slot name="prompt"></slot>
|
|
214
|
+
<slot @slotchange=${this.updateGradability}></slot> `;
|
|
215
|
+
}
|
|
216
|
+
}
|