@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.
Files changed (167) hide show
  1. package/.github/workflows/publish.yml +8 -0
  2. package/LICENSE +6 -6
  3. package/README.md +778 -0
  4. package/assets/icons/grip-horizontal.svg +8 -0
  5. package/assets/icons/grip-vertical.svg +8 -0
  6. package/assets/icons/truefalse.svg +6 -0
  7. package/custom-elements.json +2779 -0
  8. package/custom.d.ts +180 -176
  9. package/dist/api/index.d.ts +27 -0
  10. package/dist/api/index.js +40 -0
  11. package/dist/migrate.js +1434 -0
  12. package/dist/widgets/webwriter-choice-item.js +4036 -3895
  13. package/dist/widgets/webwriter-choice.js +2269 -5821
  14. package/dist/widgets/webwriter-gap-item.js +3987 -0
  15. package/dist/widgets/webwriter-gap.js +2908 -0
  16. package/dist/widgets/webwriter-mark.js +4335 -3489
  17. package/dist/widgets/webwriter-order-item.js +4014 -3306
  18. package/dist/widgets/webwriter-order.js +2944 -4386
  19. package/dist/widgets/webwriter-pairing-item.js +1058 -2421
  20. package/dist/widgets/webwriter-pairing.js +2993 -4383
  21. package/dist/widgets/webwriter-quiz-hint.js +2469 -0
  22. package/dist/widgets/webwriter-quiz.js +4933 -6099
  23. package/dist/widgets/webwriter-speech.js +6077 -5230
  24. package/dist/widgets/webwriter-task-prompt.js +4775 -2165
  25. package/dist/widgets/webwriter-task.js +2364 -8382
  26. package/dist/widgets/webwriter-text.js +4169 -4337
  27. package/dist/widgets/webwriter-true-false.js +4674 -0
  28. package/editing-config.json +106 -0
  29. package/lit-localize.json +21 -15
  30. package/localization/de.xlf +407 -74
  31. package/localization/es.xlf +407 -69
  32. package/localization/fr.xlf +407 -69
  33. package/localization/generated/de.ts +112 -29
  34. package/localization/generated/es.ts +112 -29
  35. package/localization/generated/fr.ts +112 -29
  36. package/localization/generated/index.d.ts +5 -0
  37. package/localization/generated/index.js +2 -2
  38. package/localization/generated/it.ts +112 -29
  39. package/localization/generated/locale-codes.js +31 -79
  40. package/localization/generated/nl.ts +112 -29
  41. package/localization/it.xlf +407 -69
  42. package/localization/nl.xlf +407 -69
  43. package/package.json +119 -252
  44. package/scripts/apply-translations.mjs +97 -0
  45. package/src/api/index.ts +72 -0
  46. package/src/lib/artboard.ts +686 -0
  47. package/src/lib/draggable-grid.ts +432 -0
  48. package/src/lib/encrypted-property.ts +202 -0
  49. package/src/lib/hint-popup.ts +152 -0
  50. package/src/lib/utils.ts +97 -0
  51. package/src/lib/webwriter-quirks.ts +121 -0
  52. package/src/migrate/index.ts +622 -0
  53. package/src/migrate/legacy-solution.ts +362 -0
  54. package/src/snippets/order.html +9 -8
  55. package/src/widgets/choice/webwriter-choice-item.ts +316 -0
  56. package/src/widgets/choice/webwriter-choice.ts +328 -0
  57. package/src/widgets/choice/webwriter-true-false.ts +113 -0
  58. package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
  59. package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
  60. package/src/widgets/dnd/webwriter-dnd.ts +36 -0
  61. package/src/widgets/gap/webwriter-gap-item.ts +250 -0
  62. package/src/widgets/gap/webwriter-gap.ts +447 -0
  63. package/src/widgets/order/webwriter-order-item.ts +243 -0
  64. package/src/widgets/order/webwriter-order.ts +282 -0
  65. package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
  66. package/src/widgets/pairing/webwriter-pairing.ts +934 -0
  67. package/src/widgets/speech/audio-player.ts +325 -0
  68. package/src/widgets/speech/audio-recorder.ts +285 -0
  69. package/src/widgets/speech/waveform.ts +113 -0
  70. package/src/widgets/speech/webwriter-speech.ts +87 -0
  71. package/src/widgets/webwriter-mark.ts +374 -432
  72. package/src/widgets/webwriter-quiz-hint.ts +36 -0
  73. package/src/widgets/webwriter-quiz.ts +320 -282
  74. package/src/widgets/webwriter-task-prompt.ts +49 -24
  75. package/src/widgets/webwriter-task.ts +216 -547
  76. package/src/widgets/webwriter-text.ts +291 -156
  77. package/tsconfig.api.json +12 -0
  78. package/tsconfig.json +8 -7
  79. package/dist/widgets/webwriter-choice.css +0 -444
  80. package/dist/widgets/webwriter-cloze-gap.css +0 -444
  81. package/dist/widgets/webwriter-cloze-gap.js +0 -5866
  82. package/dist/widgets/webwriter-cloze.css +0 -444
  83. package/dist/widgets/webwriter-cloze.js +0 -3786
  84. package/dist/widgets/webwriter-mark.css +0 -444
  85. package/dist/widgets/webwriter-order.css +0 -444
  86. package/dist/widgets/webwriter-pairing-item.css +0 -444
  87. package/dist/widgets/webwriter-pairing.css +0 -444
  88. package/dist/widgets/webwriter-quiz.css +0 -444
  89. package/dist/widgets/webwriter-task-explainer.css +0 -444
  90. package/dist/widgets/webwriter-task-explainer.js +0 -2872
  91. package/dist/widgets/webwriter-task-hint.css +0 -444
  92. package/dist/widgets/webwriter-task-hint.js +0 -2811
  93. package/dist/widgets/webwriter-task.css +0 -444
  94. package/dist/widgets/webwriter-text.css +0 -444
  95. package/localization/bg.xlf +0 -69
  96. package/localization/cs.xlf +0 -69
  97. package/localization/da.xlf +0 -69
  98. package/localization/el.xlf +0 -69
  99. package/localization/et.xlf +0 -69
  100. package/localization/fi.xlf +0 -69
  101. package/localization/generated/bg.ts +0 -30
  102. package/localization/generated/cs.ts +0 -30
  103. package/localization/generated/da.ts +0 -30
  104. package/localization/generated/el.ts +0 -30
  105. package/localization/generated/et.ts +0 -30
  106. package/localization/generated/fi.ts +0 -30
  107. package/localization/generated/hu.ts +0 -30
  108. package/localization/generated/id.ts +0 -30
  109. package/localization/generated/ja.ts +0 -30
  110. package/localization/generated/ko.ts +0 -30
  111. package/localization/generated/lt.ts +0 -30
  112. package/localization/generated/lv.ts +0 -30
  113. package/localization/generated/nb.ts +0 -30
  114. package/localization/generated/pl.ts +0 -30
  115. package/localization/generated/pt-BR.ts +0 -30
  116. package/localization/generated/pt-PT.ts +0 -30
  117. package/localization/generated/ro.ts +0 -30
  118. package/localization/generated/ru.ts +0 -30
  119. package/localization/generated/sk.ts +0 -30
  120. package/localization/generated/sl.ts +0 -30
  121. package/localization/generated/sv.ts +0 -30
  122. package/localization/generated/tr.ts +0 -30
  123. package/localization/generated/uk.ts +0 -30
  124. package/localization/generated/zh-hans.ts +0 -30
  125. package/localization/hu.xlf +0 -69
  126. package/localization/id.xlf +0 -69
  127. package/localization/ja.xlf +0 -69
  128. package/localization/ko.xlf +0 -69
  129. package/localization/lt.xlf +0 -69
  130. package/localization/lv.xlf +0 -69
  131. package/localization/nb.xlf +0 -69
  132. package/localization/pl.xlf +0 -69
  133. package/localization/pt-BR.xlf +0 -69
  134. package/localization/pt-PT.xlf +0 -69
  135. package/localization/ro.xlf +0 -69
  136. package/localization/ru.xlf +0 -69
  137. package/localization/sk.xlf +0 -69
  138. package/localization/sl.xlf +0 -69
  139. package/localization/sv.xlf +0 -69
  140. package/localization/tr.xlf +0 -69
  141. package/localization/uk.xlf +0 -69
  142. package/localization/zh-hans.xlf +0 -69
  143. package/src/lib/combobox.ts +0 -456
  144. package/src/lib/highlighter-fill.svg +0 -6
  145. package/src/snippets/Beispiel-Choice.html +0 -57
  146. package/src/snippets/Beispiel-Mark.html +0 -14
  147. package/src/snippets/Beispiel-Order.html +0 -52
  148. package/src/snippets/Beispiel-Text.html +0 -20
  149. package/src/snippets/blank.html +0 -1
  150. package/src/snippets/choice.html +0 -8
  151. package/src/snippets/cloze.html +0 -4
  152. package/src/snippets/mark.html +0 -4
  153. package/src/snippets/pairing.html +0 -10
  154. package/src/snippets/speech.html +0 -4
  155. package/src/snippets/text.html +0 -4
  156. package/src/snippets/wordsearch.html +0 -4
  157. package/src/widgets/webwriter-choice-item.ts +0 -254
  158. package/src/widgets/webwriter-choice.ts +0 -353
  159. package/src/widgets/webwriter-cloze-gap.ts +0 -216
  160. package/src/widgets/webwriter-cloze.ts +0 -136
  161. package/src/widgets/webwriter-order-item.ts +0 -441
  162. package/src/widgets/webwriter-order.ts +0 -327
  163. package/src/widgets/webwriter-pairing-item.ts +0 -161
  164. package/src/widgets/webwriter-pairing.ts +0 -193
  165. package/src/widgets/webwriter-speech.ts +0 -273
  166. package/src/widgets/webwriter-task-explainer.ts +0 -43
  167. 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
+ }