@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,328 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
|
|
3
|
+
import { action, LitElementWw, type OptionDeclaration } from "@webwriter/lit";
|
|
4
|
+
import PlusIcon from "bootstrap-icons/icons/plus.svg";
|
|
5
|
+
import UiChecksIcon from "bootstrap-icons/icons/ui-checks.svg";
|
|
6
|
+
import UiRadiosIcon from "bootstrap-icons/icons/ui-radios.svg";
|
|
7
|
+
import { css, html, nothing, PropertyValues } from "lit";
|
|
8
|
+
import { customElement, property, queryAssignedElements, state } from "lit/decorators.js";
|
|
9
|
+
import { name as packageId } from "../../../package.json";
|
|
10
|
+
import { IWebWriterQuizType, registerQuizType } from "../../api";
|
|
11
|
+
import { DraggableGrid, DraggableGridReorderEvent } from "../../lib/draggable-grid";
|
|
12
|
+
import { encryptedProperty, encryptPlaintextAttributes } from "../../lib/encrypted-property";
|
|
13
|
+
import { randomizeArray } from "../../lib/utils";
|
|
14
|
+
import { mutateWithStableSelection, registerAsEnterCreatesNew } from "../../lib/webwriter-quirks";
|
|
15
|
+
import type { WebwriterChoiceItem } from "./webwriter-choice-item";
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
interface HTMLElementTagNameMap {
|
|
19
|
+
"webwriter-choice": WebwriterChoice;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ChoiceMode = "single" | "multiple";
|
|
24
|
+
export type ChoiceLayout = "list" | "tiles";
|
|
25
|
+
export type ChoiceGradingMethod = "pass-fail" | "partial";
|
|
26
|
+
export type ChoiceSolution = { [id: string]: boolean };
|
|
27
|
+
|
|
28
|
+
registerQuizType({
|
|
29
|
+
packageId,
|
|
30
|
+
widgetPosition: 2,
|
|
31
|
+
id: "webwriter-single-choice",
|
|
32
|
+
getName: () => msg("Single Choice"),
|
|
33
|
+
icon: UiRadiosIcon,
|
|
34
|
+
createInstance: () => {
|
|
35
|
+
const choice = document.createElement("webwriter-choice");
|
|
36
|
+
choice.setAttribute("mode", "single");
|
|
37
|
+
return choice;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
registerQuizType({
|
|
42
|
+
packageId,
|
|
43
|
+
widgetPosition: 3,
|
|
44
|
+
id: "webwriter-multiple-choice",
|
|
45
|
+
getName: () => msg("Multiple Choice"),
|
|
46
|
+
icon: UiChecksIcon,
|
|
47
|
+
createInstance: () => {
|
|
48
|
+
const choice = document.createElement("webwriter-choice");
|
|
49
|
+
choice.setAttribute("mode", "multiple");
|
|
50
|
+
return choice;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* An answer where learners pick one or more options.
|
|
56
|
+
*
|
|
57
|
+
* The options must be `<webwriter-choice-item>` elements assigned to the
|
|
58
|
+
* default slot.
|
|
59
|
+
*/
|
|
60
|
+
@customElement("webwriter-choice")
|
|
61
|
+
export class WebwriterChoice extends LitElementWw implements IWebWriterQuizType {
|
|
62
|
+
/** @internal */
|
|
63
|
+
get dynamicOptions(): Record<string, OptionDeclaration> {
|
|
64
|
+
return {
|
|
65
|
+
"mode": {
|
|
66
|
+
type: "select",
|
|
67
|
+
options: [
|
|
68
|
+
{ value: "single", label: { _: msg("Single Choice") } },
|
|
69
|
+
{ value: "multiple", label: { _: msg("Multiple Choice") } },
|
|
70
|
+
],
|
|
71
|
+
label: { _: msg("Mode") },
|
|
72
|
+
},
|
|
73
|
+
"layout": {
|
|
74
|
+
type: "select",
|
|
75
|
+
options: [
|
|
76
|
+
{ value: "list", label: { _: msg("List") } },
|
|
77
|
+
{ value: "tiles", label: { _: msg("Tiles") } },
|
|
78
|
+
],
|
|
79
|
+
label: { _: msg("Layout") },
|
|
80
|
+
},
|
|
81
|
+
"randomize-order": { type: "boolean", label: { _: msg("Randomize Order") } },
|
|
82
|
+
"grading-method": {
|
|
83
|
+
type: "select",
|
|
84
|
+
options: [
|
|
85
|
+
{ value: "pass-fail", label: { _: msg("Pass/Fail") } },
|
|
86
|
+
{ value: "partial", label: { _: msg("Partial Credit") } },
|
|
87
|
+
],
|
|
88
|
+
label: { _: msg("Grading Method") },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @internal */
|
|
94
|
+
static scopedElements = {
|
|
95
|
+
"sl-icon": SlIcon,
|
|
96
|
+
"draggable-grid": DraggableGrid,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
static styles = css`
|
|
100
|
+
.container-list {
|
|
101
|
+
display: grid;
|
|
102
|
+
grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.container-tiles {
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
|
108
|
+
gap: var(--sl-spacing-medium);
|
|
109
|
+
|
|
110
|
+
.add-option {
|
|
111
|
+
aspect-ratio: 1 / 1;
|
|
112
|
+
|
|
113
|
+
border: 1px dashed var(--sl-color-neutral-300);
|
|
114
|
+
border-radius: var(--sl-border-radius-medium);
|
|
115
|
+
box-sizing: border-box;
|
|
116
|
+
color: var(--sl-color-neutral-500);
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
gap: 0.5rem;
|
|
123
|
+
|
|
124
|
+
&:hover {
|
|
125
|
+
background-color: var(--sl-color-primary-50);
|
|
126
|
+
border-color: var(--sl-color-primary-300);
|
|
127
|
+
color: var(--sl-color-primary-700);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* How many options learners may select.
|
|
135
|
+
*
|
|
136
|
+
* - `single`: Exactly one option can be selected.
|
|
137
|
+
* - `multiple`: Any number of options can be selected.
|
|
138
|
+
*/
|
|
139
|
+
@property({ type: String, attribute: "mode", reflect: true })
|
|
140
|
+
accessor mode: ChoiceMode = "single";
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* How the options are arranged.
|
|
144
|
+
*
|
|
145
|
+
* - `list`: One option per row.
|
|
146
|
+
* - `tiles`: A responsive grid of equally sized tiles.
|
|
147
|
+
*/
|
|
148
|
+
@property({ type: String, attribute: "layout", reflect: true })
|
|
149
|
+
accessor layout: ChoiceLayout = "list";
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* If true, the order of options will be randomized for learners.
|
|
153
|
+
*
|
|
154
|
+
* Note: Randomization occurs only during the first render, so changing this property dynamically will not have any effect.
|
|
155
|
+
*/
|
|
156
|
+
@property({ type: Boolean, attribute: "randomize-order", reflect: true })
|
|
157
|
+
private accessor randomizeOrder: boolean = false;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* How the score of this answer is calculated.
|
|
161
|
+
*
|
|
162
|
+
* - `pass-fail`: Full score only if exactly the correct options are selected.
|
|
163
|
+
* - `partial`: Correct selections earn credit, wrong ones subtract from it.
|
|
164
|
+
*/
|
|
165
|
+
@property({ type: String, attribute: "grading-method", reflect: true })
|
|
166
|
+
accessor gradingMethod: ChoiceGradingMethod = "pass-fail";
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The options counting as correct, as a map from option ID to whether it has to be selected.
|
|
170
|
+
* It is obfuscated in the markup so that learners cannot read it directly.
|
|
171
|
+
*/
|
|
172
|
+
@property({ type: Array, attribute: "solution", reflect: true, converter: encryptedProperty })
|
|
173
|
+
accessor solution: ChoiceSolution = {};
|
|
174
|
+
|
|
175
|
+
@state() private accessor currentAnswer: ChoiceSolution = {};
|
|
176
|
+
@state() private accessor graded: boolean = false;
|
|
177
|
+
@state() private accessor detailedFeedback: boolean = false;
|
|
178
|
+
|
|
179
|
+
@queryAssignedElements()
|
|
180
|
+
private accessor items!: WebwriterChoiceItem[];
|
|
181
|
+
|
|
182
|
+
private syncSolutionWithItems() {
|
|
183
|
+
const newSolution: ChoiceSolution = {};
|
|
184
|
+
for (const item of this.items) newSolution[item.id] = this.solution[item.id] || false;
|
|
185
|
+
this.solution = newSolution;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private randomizeItems() {
|
|
189
|
+
this.append(...randomizeArray(this.items));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private get selected(): ChoiceSolution {
|
|
193
|
+
return this.isContentEditable ? this.solution : this.currentAnswer;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private set selected(value: ChoiceSolution) {
|
|
197
|
+
if (this.isContentEditable) this.solution = value;
|
|
198
|
+
else this.currentAnswer = value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @internal */
|
|
202
|
+
getChoiceState(id: string) {
|
|
203
|
+
const correct = this.solution[id] === (this.currentAnswer[id] ?? false);
|
|
204
|
+
return {
|
|
205
|
+
selected: this.selected[id] ?? false,
|
|
206
|
+
disabled: this.graded,
|
|
207
|
+
showFeedback: this.graded && this.detailedFeedback,
|
|
208
|
+
correct: this.graded && this.detailedFeedback && correct,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected firstUpdated(changed: PropertyValues): void {
|
|
213
|
+
super.firstUpdated(changed);
|
|
214
|
+
encryptPlaintextAttributes(this);
|
|
215
|
+
this.syncSolutionWithItems();
|
|
216
|
+
// We assume randomizeOrder does not change during learner view, so shuffle only once.
|
|
217
|
+
// TODO: Actually handle dynamic changes to randomizeOrder
|
|
218
|
+
if (this.randomizeOrder && !this.isContentEditable) this.randomizeItems();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected updated(changed: PropertyValues): void {
|
|
222
|
+
if (changed.has("mode") && this.mode === "single") {
|
|
223
|
+
// Ensure that the first item is selected if multiple were selected before switching to single mode
|
|
224
|
+
const newSelected: ChoiceSolution = {};
|
|
225
|
+
let isFirst = true;
|
|
226
|
+
for (const item of this.items) {
|
|
227
|
+
if (this.selected[item.id] && isFirst) {
|
|
228
|
+
newSelected[item.id] = true;
|
|
229
|
+
isFirst = false;
|
|
230
|
+
} else {
|
|
231
|
+
newSelected[item.id] = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this.selected = newSelected;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (changed.has("solution") || changed.has("currentAnswer") || changed.has("graded")) {
|
|
238
|
+
this.items.forEach(item => item.requestUpdate?.());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private handleReorder({ detail }: DraggableGridReorderEvent) {
|
|
243
|
+
mutateWithStableSelection(detail.item, () => this.insertBefore(detail.item, detail.before));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private handleSelect(event: CustomEvent) {
|
|
247
|
+
if (this.graded) return;
|
|
248
|
+
const id = (event.target as WebwriterChoiceItem).id;
|
|
249
|
+
if (this.mode === "single") {
|
|
250
|
+
const newSelected: { [key: string]: boolean } = {};
|
|
251
|
+
for (const key in this.selected) newSelected[key] = key === id;
|
|
252
|
+
this.selected = newSelected;
|
|
253
|
+
} else {
|
|
254
|
+
this.selected = { ...this.selected, [id]: !this.selected[id] };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
connectedCallback() {
|
|
259
|
+
super.connectedCallback();
|
|
260
|
+
registerAsEnterCreatesNew("webwriter-choice-item");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@action({ label: { _: msg("Add Option") } })
|
|
264
|
+
private addOption() {
|
|
265
|
+
this.appendChild(document.createElement("webwriter-choice-item"));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
checkValidity(): boolean {
|
|
269
|
+
return Object.values(this.currentAnswer).some(v => v === true);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
checkAnswer(detailedFeedback: boolean): number {
|
|
273
|
+
this.graded = true;
|
|
274
|
+
this.detailedFeedback = detailedFeedback;
|
|
275
|
+
|
|
276
|
+
const toSet = (map: ChoiceSolution) =>
|
|
277
|
+
new Set(
|
|
278
|
+
Object.entries(map)
|
|
279
|
+
.filter(([, correct]) => correct)
|
|
280
|
+
.map(([id]) => id),
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const correct = toSet(this.solution);
|
|
284
|
+
const selected = toSet(this.selected);
|
|
285
|
+
|
|
286
|
+
const selectedCorrect = selected.intersection(correct);
|
|
287
|
+
const selectedWrong = selected.difference(correct);
|
|
288
|
+
|
|
289
|
+
if (this.gradingMethod === "pass-fail") {
|
|
290
|
+
return selectedWrong.size === 0 && selectedCorrect.size === correct.size ? 1 : 0;
|
|
291
|
+
} else if (this.gradingMethod === "partial") {
|
|
292
|
+
return Math.max(0, selectedCorrect.size - selectedWrong.size) / correct.size;
|
|
293
|
+
} else {
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
reset() {
|
|
299
|
+
const newSelected: ChoiceSolution = {};
|
|
300
|
+
for (const item of this.items) newSelected[item.id] = false;
|
|
301
|
+
this.selected = newSelected;
|
|
302
|
+
|
|
303
|
+
if (this.randomizeOrder && !this.isContentEditable) this.randomizeItems();
|
|
304
|
+
this.graded = false;
|
|
305
|
+
this.detailedFeedback = false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private AddOptionButton() {
|
|
309
|
+
if (this.layout !== "tiles" || !this.isContentEditable) return nothing;
|
|
310
|
+
return html`
|
|
311
|
+
<div class="add-option" @click=${this.addOption} id="add-option" data-not-draggable>
|
|
312
|
+
<sl-icon src=${PlusIcon}></sl-icon>
|
|
313
|
+
Add Option
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
render() {
|
|
319
|
+
return html`<draggable-grid
|
|
320
|
+
class="container-${this.layout}"
|
|
321
|
+
?vertical=${this.layout === "list"}
|
|
322
|
+
@reorder=${this.handleReorder}
|
|
323
|
+
>
|
|
324
|
+
<slot @select=${this.handleSelect} @slotchange=${this.syncSolutionWithItems}></slot>
|
|
325
|
+
${this.AddOptionButton()}
|
|
326
|
+
</draggable-grid>`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js";
|
|
3
|
+
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js";
|
|
4
|
+
import { LitElementWw } from "@webwriter/lit";
|
|
5
|
+
import { registerQuizType, type IWebWriterQuizType } from "../../api";
|
|
6
|
+
import { css, html } from "lit";
|
|
7
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
8
|
+
import LOCALIZE from "../../../localization/generated";
|
|
9
|
+
import { name as packageId } from "../../../package.json";
|
|
10
|
+
import TrueFalseIcon from "../../../assets/icons/truefalse.svg";
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
interface HTMLElementTagNameMap {
|
|
14
|
+
"webwriter-true-false": WebwriterTrueFalse;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type TrueFalse = "true" | "false";
|
|
19
|
+
|
|
20
|
+
registerQuizType({
|
|
21
|
+
packageId,
|
|
22
|
+
widgetPosition: 1,
|
|
23
|
+
id: "webwriter-true-false",
|
|
24
|
+
getName: () => msg("True or False"),
|
|
25
|
+
icon: TrueFalseIcon,
|
|
26
|
+
createInstance: () => document.createElement("webwriter-true-false"),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An answer where learners decide whether a statement is true or false.
|
|
31
|
+
*
|
|
32
|
+
* It must be assigned to the default slot of a `<webwriter-task>`.
|
|
33
|
+
*/
|
|
34
|
+
@customElement("webwriter-true-false")
|
|
35
|
+
export class WebwriterTrueFalse extends LitElementWw implements IWebWriterQuizType {
|
|
36
|
+
/** @internal */
|
|
37
|
+
localize = LOCALIZE;
|
|
38
|
+
|
|
39
|
+
/** @internal */
|
|
40
|
+
static scopedElements = {
|
|
41
|
+
"sl-button": SlButton,
|
|
42
|
+
"sl-button-group": SlButtonGroup,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
static styles = css`
|
|
46
|
+
sl-button:disabled::part(base) {
|
|
47
|
+
opacity: 1 !important;
|
|
48
|
+
cursor: default !important;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The value counting as correct.
|
|
54
|
+
*
|
|
55
|
+
* - `true`: The statement is true.
|
|
56
|
+
* - `false`: The statement is false.
|
|
57
|
+
*/
|
|
58
|
+
@property({ type: String, attribute: true, reflect: true })
|
|
59
|
+
accessor solution: TrueFalse = "true";
|
|
60
|
+
|
|
61
|
+
@state() private accessor currentAnswer: TrueFalse | undefined;
|
|
62
|
+
@state() private accessor graded: boolean = false;
|
|
63
|
+
@state() private accessor showFeedback: boolean = false;
|
|
64
|
+
|
|
65
|
+
checkValidity() {
|
|
66
|
+
return this.currentAnswer !== undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reset() {
|
|
70
|
+
this.graded = false;
|
|
71
|
+
this.currentAnswer = undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
checkAnswer(detailedFeedback: boolean) {
|
|
75
|
+
this.graded = true;
|
|
76
|
+
this.showFeedback = detailedFeedback;
|
|
77
|
+
return this.currentAnswer === this.solution ? 1 : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private get selected() {
|
|
81
|
+
return this.isContentEditable ? this.solution : this.currentAnswer;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private set selected(value: TrueFalse | undefined) {
|
|
85
|
+
if (this.isContentEditable) this.solution = value ?? "true";
|
|
86
|
+
else this.currentAnswer = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private Button(choice: TrueFalse, label: string) {
|
|
90
|
+
let variant = "default";
|
|
91
|
+
if (this.selected === choice) {
|
|
92
|
+
if (this.graded && this.showFeedback) {
|
|
93
|
+
variant = this.solution === choice ? "success" : "danger";
|
|
94
|
+
} else {
|
|
95
|
+
variant = "primary";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return html`<sl-button
|
|
100
|
+
variant=${variant}
|
|
101
|
+
@click=${() => (this.selected = choice)}
|
|
102
|
+
?disabled=${this.graded}
|
|
103
|
+
>
|
|
104
|
+
${label}
|
|
105
|
+
</sl-button>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render() {
|
|
109
|
+
return html`<sl-button-group>
|
|
110
|
+
${this.Button("true", msg("True"))} ${this.Button("false", msg("False"))}
|
|
111
|
+
</sl-button-group>`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { css, html } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
|
+
import { ArtboardItemElement } from "../../lib/artboard";
|
|
4
|
+
|
|
5
|
+
@customElement("webwriter-dnd-draggable")
|
|
6
|
+
export class WebWriterDragAndDropDraggable extends ArtboardItemElement {
|
|
7
|
+
hasEditableContent = true;
|
|
8
|
+
minimumWidth = 5;
|
|
9
|
+
minimumHeight = 5;
|
|
10
|
+
|
|
11
|
+
static styles = css`
|
|
12
|
+
:host {
|
|
13
|
+
background-color: var(--sl-color-neutral-0);
|
|
14
|
+
border-radius: var(--sl-border-radius-medium);
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
display: block;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
padding: 5px var(--sl-spacing-x-small);
|
|
20
|
+
border: 1px solid var(--sl-color-neutral-300);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
render() {
|
|
26
|
+
return html`<slot></slot>`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { customElement } from "lit/decorators.js";
|
|
2
|
+
import { ArtboardItemElement } from "../../lib/artboard";
|
|
3
|
+
|
|
4
|
+
@customElement("webwriter-dnd-dropzone")
|
|
5
|
+
export class WebWriterDragAndDropDropzone extends ArtboardItemElement {
|
|
6
|
+
hasEditableContent = false;
|
|
7
|
+
minimumWidth = 5;
|
|
8
|
+
minimumHeight = 5;
|
|
9
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import HandIndexThumbIcon from "bootstrap-icons/icons/hand-index-thumb.svg";
|
|
3
|
+
import { customElement } from "lit/decorators.js";
|
|
4
|
+
import { name as packageId } from "../../../package.json";
|
|
5
|
+
import { IWebWriterQuizType, registerQuizType } from "../../api";
|
|
6
|
+
import { ArtboardContainerElement } from "../../lib/artboard";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
"webwriter-dnd": WebWriterDragAndDrop;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
registerQuizType({
|
|
15
|
+
packageId,
|
|
16
|
+
widgetPosition: 20,
|
|
17
|
+
id: "webwriter-dnd",
|
|
18
|
+
getName: () => msg("Drag and Drop"),
|
|
19
|
+
icon: HandIndexThumbIcon,
|
|
20
|
+
createInstance: () => document.createElement("webwriter-dnd"),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
@customElement("webwriter-dnd")
|
|
24
|
+
export class WebWriterDragAndDrop extends ArtboardContainerElement implements IWebWriterQuizType {
|
|
25
|
+
protected readonly itemElementTags = ["webwriter-dnd-draggable", "webwriter-dnd-dropzone"];
|
|
26
|
+
|
|
27
|
+
checkValidity(): boolean {
|
|
28
|
+
throw new Error("Method not implemented.");
|
|
29
|
+
}
|
|
30
|
+
reset(): void {
|
|
31
|
+
throw new Error("Method not implemented.");
|
|
32
|
+
}
|
|
33
|
+
checkAnswer?(detailedFeedback: boolean): number {
|
|
34
|
+
throw new Error("Method not implemented.");
|
|
35
|
+
}
|
|
36
|
+
}
|