@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,243 @@
|
|
|
1
|
+
import { msg } 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 { LitElementWw } from "@webwriter/lit";
|
|
5
|
+
import { css, html, nothing } from "lit";
|
|
6
|
+
import { customElement } from "lit/decorators.js";
|
|
7
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
8
|
+
import GripHorizontalIcon from "../../../assets/icons/grip-horizontal.svg";
|
|
9
|
+
import GripVerticalIcon from "../../../assets/icons/grip-vertical.svg";
|
|
10
|
+
import { DraggableGridItem } from "../../lib/draggable-grid";
|
|
11
|
+
import { HintPopup } from "../../lib/hint-popup";
|
|
12
|
+
import { commonStyles } from "../../lib/utils";
|
|
13
|
+
import { WebwriterOrder } from "./webwriter-order";
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
interface HTMLElementTagNameMap {
|
|
17
|
+
"webwriter-order-item": WebwriterOrderItem;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ROMAN_NUMERALS_TABLE: [number, string][] = [
|
|
22
|
+
[1000, "M"],
|
|
23
|
+
[900, "CM"],
|
|
24
|
+
[500, "D"],
|
|
25
|
+
[400, "CD"],
|
|
26
|
+
[100, "C"],
|
|
27
|
+
[90, "XC"],
|
|
28
|
+
[50, "L"],
|
|
29
|
+
[40, "XL"],
|
|
30
|
+
[10, "X"],
|
|
31
|
+
[9, "IX"],
|
|
32
|
+
[5, "V"],
|
|
33
|
+
[4, "IV"],
|
|
34
|
+
[1, "I"],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A single item of a `<webwriter-order>`.
|
|
39
|
+
*
|
|
40
|
+
* It must be assigned to the default slot of its parent element. An optional
|
|
41
|
+
* `<webwriter-quiz-hint>` may be assigned to its `hint` slot.
|
|
42
|
+
*/
|
|
43
|
+
@customElement("webwriter-order-item")
|
|
44
|
+
export class WebwriterOrderItem extends LitElementWw implements DraggableGridItem {
|
|
45
|
+
static styles = [
|
|
46
|
+
commonStyles,
|
|
47
|
+
css`
|
|
48
|
+
:host {
|
|
49
|
+
display: contents !important;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
slot {
|
|
53
|
+
display: block;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.order-item {
|
|
57
|
+
background-color: var(--sl-color-neutral-0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.layout-list {
|
|
61
|
+
display: grid !important;
|
|
62
|
+
grid-column: 1 / -1;
|
|
63
|
+
grid-template-columns: subgrid;
|
|
64
|
+
|
|
65
|
+
> * {
|
|
66
|
+
padding: var(--sl-spacing-2x-small) 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.actions,
|
|
71
|
+
hint-popup::part(empty) {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
:is(.ww-dragging, :host(:hover)) .actions {
|
|
76
|
+
opacity: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:is(.ww-dragging, :host(.ww-selected-text-within), :host(:hover)) hint-popup::part(empty) {
|
|
80
|
+
opacity: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.layout-tiles {
|
|
84
|
+
border: 1px solid var(--sl-input-border-color);
|
|
85
|
+
border-radius: var(--sl-border-radius-large);
|
|
86
|
+
padding: var(--sl-spacing-small);
|
|
87
|
+
box-sizing: border-box;
|
|
88
|
+
|
|
89
|
+
display: grid !important;
|
|
90
|
+
grid-template-columns: auto 1fr max-content max-content;
|
|
91
|
+
grid-template-rows: auto 1fr;
|
|
92
|
+
grid-template-areas: "position . hint grab" "content content content content";
|
|
93
|
+
position: relative;
|
|
94
|
+
aspect-ratio: 1 / 1;
|
|
95
|
+
|
|
96
|
+
.position {
|
|
97
|
+
grid-area: position;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.content {
|
|
101
|
+
grid-area: content;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
hint-popup {
|
|
105
|
+
grid-area: hint;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.grip {
|
|
109
|
+
grid-area: grab;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.actions {
|
|
113
|
+
position: absolute;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.actions {
|
|
117
|
+
position: absolute;
|
|
118
|
+
background: var(--sl-color-neutral-0);
|
|
119
|
+
border: 1px solid var(--sl-input-border-color);
|
|
120
|
+
border-radius: var(--sl-border-radius-pill);
|
|
121
|
+
top: 0;
|
|
122
|
+
left: 50%;
|
|
123
|
+
translate: -50% -50%;
|
|
124
|
+
padding: 0 var(--sl-spacing-x-small);
|
|
125
|
+
box-sizing: border-box;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
:host(:not([contenteditable="true"]):not([contenteditable=""])) {
|
|
130
|
+
.order-item:hover .grip {
|
|
131
|
+
color: var(--sl-color-primary-600);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.order-item.ww-dragging .grip {
|
|
135
|
+
color: var(--sl-color-primary-700);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.grip,
|
|
140
|
+
.position {
|
|
141
|
+
padding-right: var(--sl-spacing-small);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.position {
|
|
145
|
+
font-variant-numeric: tabular-nums;
|
|
146
|
+
font-weight: bold;
|
|
147
|
+
|
|
148
|
+
&.numbering-numeric {
|
|
149
|
+
text-align: right;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
&.numbering-none {
|
|
153
|
+
padding: 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.actions,
|
|
158
|
+
.grip {
|
|
159
|
+
height: 1lh;
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
[data-drag-handle] {
|
|
165
|
+
cursor: grab;
|
|
166
|
+
touch-action: none;
|
|
167
|
+
}
|
|
168
|
+
`,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/** @internal */
|
|
172
|
+
get draggableElement(): HTMLElement {
|
|
173
|
+
return this.shadowRoot?.querySelector(".order-item") as HTMLElement;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @internal */
|
|
177
|
+
static scopedElements = {
|
|
178
|
+
"sl-icon": SlIcon,
|
|
179
|
+
"hint-popup": HintPopup,
|
|
180
|
+
"sl-icon-button": SlIconButton,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** @internal */
|
|
184
|
+
getPositionText(): string {
|
|
185
|
+
const parent = this.parentElement as WebwriterOrder | undefined;
|
|
186
|
+
if (!parent) return "";
|
|
187
|
+
|
|
188
|
+
let index = parent.items.indexOf(this);
|
|
189
|
+
if (index === -1) return "";
|
|
190
|
+
|
|
191
|
+
if (parent.direction === "descending") index = parent.items.length - 1 - index;
|
|
192
|
+
|
|
193
|
+
switch (parent.numberingStyle) {
|
|
194
|
+
case "numeric":
|
|
195
|
+
return (index + 1).toString() + ".";
|
|
196
|
+
case "alphabetic":
|
|
197
|
+
let result = "";
|
|
198
|
+
do {
|
|
199
|
+
result = String.fromCharCode((index % 26) + 65) + result;
|
|
200
|
+
index = Math.floor(index / 26) - 1;
|
|
201
|
+
} while (index >= 0);
|
|
202
|
+
return result + ".";
|
|
203
|
+
case "roman":
|
|
204
|
+
return (
|
|
205
|
+
ROMAN_NUMERALS_TABLE.reduce((acc, [value, numeral]) => {
|
|
206
|
+
while (index + 1 >= value) {
|
|
207
|
+
acc += numeral;
|
|
208
|
+
index -= value;
|
|
209
|
+
}
|
|
210
|
+
return acc;
|
|
211
|
+
}, "") + "."
|
|
212
|
+
);
|
|
213
|
+
default:
|
|
214
|
+
case "none":
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
render() {
|
|
220
|
+
const parent = this.parentElement as WebwriterOrder | undefined;
|
|
221
|
+
if (!parent) return nothing;
|
|
222
|
+
|
|
223
|
+
const gripIcon = parent.layout === "list" ? GripVerticalIcon : GripHorizontalIcon;
|
|
224
|
+
|
|
225
|
+
return html`
|
|
226
|
+
<div
|
|
227
|
+
class=${classMap({
|
|
228
|
+
"order-item": true,
|
|
229
|
+
[`layout-${parent.layout}`]: true,
|
|
230
|
+
})}
|
|
231
|
+
?data-drag-handle=${!this.isContentEditable}
|
|
232
|
+
>
|
|
233
|
+
${this.isContentEditable ? html`<div></div>` : html`<sl-icon class="grip" src=${gripIcon}></sl-icon>`}
|
|
234
|
+
<div class="position numbering-${parent.numberingStyle}">${this.getPositionText()}</div>
|
|
235
|
+
<slot class="content" style=${`--ww-placeholder: "${msg("Option")}"`}></slot>
|
|
236
|
+
<div class="actions author-only">
|
|
237
|
+
<sl-icon-button ?data-drag-handle=${this.isContentEditable} src=${gripIcon}></sl-icon-button>
|
|
238
|
+
</div>
|
|
239
|
+
<hint-popup position="start" data-drag-no-handle><slot name="hint"></slot></hint-popup>
|
|
240
|
+
</div>
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
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 ArrowDownUpIcon from "bootstrap-icons/icons/arrow-down-up.svg";
|
|
5
|
+
import PlusIcon from "bootstrap-icons/icons/plus.svg";
|
|
6
|
+
import { css, html, nothing, PropertyValues } from "lit";
|
|
7
|
+
import { customElement, property, queryAssignedElements } from "lit/decorators.js";
|
|
8
|
+
import LOCALIZE from "../../../localization/generated";
|
|
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 { commonStyles, randomizeArray } from "../../lib/utils";
|
|
14
|
+
import { mutateWithStableSelection, registerAsEnterCreatesNew } from "../../lib/webwriter-quirks";
|
|
15
|
+
import type { WebwriterOrderItem } from "./webwriter-order-item";
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
interface HTMLElementTagNameMap {
|
|
19
|
+
"webwriter-order": WebwriterOrder;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OrderLayout = "list" | "tiles";
|
|
24
|
+
export type OrderDirection = "ascending" | "descending";
|
|
25
|
+
export type OrderNumberingStyle = "none" | "numeric" | "alphabetic" | "roman";
|
|
26
|
+
export type OrderGradingMethod = "pass-fail" | "partial-position" | "partial-sequence";
|
|
27
|
+
export type OrderSolution = string[];
|
|
28
|
+
|
|
29
|
+
registerQuizType({
|
|
30
|
+
packageId,
|
|
31
|
+
widgetPosition: 20,
|
|
32
|
+
id: "webwriter-order",
|
|
33
|
+
getName: () => msg("Order"),
|
|
34
|
+
icon: ArrowDownUpIcon,
|
|
35
|
+
createInstance: () => document.createElement("webwriter-order"),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An answer where learners drag items into the correct order.
|
|
40
|
+
*
|
|
41
|
+
* The items must be `<webwriter-order-item>` elements assigned to the default
|
|
42
|
+
* slot.
|
|
43
|
+
*/
|
|
44
|
+
@customElement("webwriter-order")
|
|
45
|
+
export class WebwriterOrder extends LitElementWw implements IWebWriterQuizType {
|
|
46
|
+
/** @internal */
|
|
47
|
+
get dynamicOptions(): Record<string, OptionDeclaration> {
|
|
48
|
+
return {
|
|
49
|
+
"layout": {
|
|
50
|
+
type: "select",
|
|
51
|
+
label: { _: msg("Layout") },
|
|
52
|
+
options: [
|
|
53
|
+
{ value: "list", label: { _: msg("List") } },
|
|
54
|
+
{ value: "tiles", label: { _: msg("Tiles") } },
|
|
55
|
+
],
|
|
56
|
+
description: { _: msg("The layout variant to use.") },
|
|
57
|
+
},
|
|
58
|
+
"direction": {
|
|
59
|
+
type: "select",
|
|
60
|
+
label: { _: msg("Direction") },
|
|
61
|
+
options: [
|
|
62
|
+
{ value: "ascending", label: { _: msg("Ascending") } },
|
|
63
|
+
{ value: "descending", label: { _: msg("Descending") } },
|
|
64
|
+
],
|
|
65
|
+
description: { _: msg("The order direction to use.") },
|
|
66
|
+
},
|
|
67
|
+
"numbering-style": {
|
|
68
|
+
type: "select",
|
|
69
|
+
label: { _: msg("Numbering") },
|
|
70
|
+
options: [
|
|
71
|
+
{ value: "none", label: { _: msg("None") } },
|
|
72
|
+
{ value: "numeric", label: { _: msg("Numeric") } },
|
|
73
|
+
{ value: "alphabetic", label: { _: msg("Alphabetic") } },
|
|
74
|
+
{ value: "roman", label: { _: msg("Roman") } },
|
|
75
|
+
],
|
|
76
|
+
description: { _: msg("The numbering style to use.") },
|
|
77
|
+
},
|
|
78
|
+
"grading-method": {
|
|
79
|
+
type: "select",
|
|
80
|
+
label: { _: msg("Grading") },
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "pass-fail", label: { _: msg("Pass/Fail") } },
|
|
83
|
+
{ value: "partial-position", label: { _: msg("Partial (Position)") } },
|
|
84
|
+
{ value: "partial-sequence", label: { _: msg("Partial (Sequence)") } },
|
|
85
|
+
],
|
|
86
|
+
description: { _: msg("The grading method to use.") },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @internal */
|
|
92
|
+
localize = LOCALIZE;
|
|
93
|
+
|
|
94
|
+
/** @internal */
|
|
95
|
+
static scopedElements = {
|
|
96
|
+
"draggable-grid": DraggableGrid,
|
|
97
|
+
"sl-icon": SlIcon,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
static styles = [
|
|
101
|
+
commonStyles,
|
|
102
|
+
css`
|
|
103
|
+
.container-list {
|
|
104
|
+
display: grid;
|
|
105
|
+
grid-template-columns: max-content max-content minmax(0, 1fr) max-content max-content;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.container-tiles {
|
|
109
|
+
display: grid;
|
|
110
|
+
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
|
111
|
+
gap: var(--sl-spacing-medium);
|
|
112
|
+
|
|
113
|
+
.add-option {
|
|
114
|
+
aspect-ratio: 1 / 1;
|
|
115
|
+
|
|
116
|
+
border: 1px dashed var(--sl-color-neutral-300);
|
|
117
|
+
border-radius: var(--sl-border-radius-medium);
|
|
118
|
+
box-sizing: border-box;
|
|
119
|
+
color: var(--sl-color-neutral-500);
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
|
|
127
|
+
&:hover {
|
|
128
|
+
background-color: var(--sl-color-primary-50);
|
|
129
|
+
border-color: var(--sl-color-primary-300);
|
|
130
|
+
color: var(--sl-color-primary-700);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* How the items are arranged.
|
|
139
|
+
*
|
|
140
|
+
* - `list`: One item per row.
|
|
141
|
+
* - `tiles`: A responsive grid of equally sized tiles.
|
|
142
|
+
*/
|
|
143
|
+
@property({ type: String, attribute: "layout", reflect: true })
|
|
144
|
+
accessor layout: OrderLayout = "list";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The direction in which the items are numbered.
|
|
148
|
+
*
|
|
149
|
+
* - `ascending`: The first item carries the lowest position.
|
|
150
|
+
* - `descending`: The first item carries the highest position.
|
|
151
|
+
*/
|
|
152
|
+
@property({ type: String, attribute: "direction", reflect: true })
|
|
153
|
+
accessor direction: OrderDirection = "ascending";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* How the position of each item is labelled.
|
|
157
|
+
*
|
|
158
|
+
* - `none`: No position is shown.
|
|
159
|
+
* - `numeric`: Arabic numerals (1., 2., 3., ...).
|
|
160
|
+
* - `alphabetic`: Latin letters (A., B., C., ...).
|
|
161
|
+
* - `roman`: Roman numerals (I., II., III., ...).
|
|
162
|
+
*/
|
|
163
|
+
@property({ type: String, attribute: "numbering-style", reflect: true })
|
|
164
|
+
accessor numberingStyle: OrderNumberingStyle = "numeric";
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* How the score of this answer is calculated.
|
|
168
|
+
*
|
|
169
|
+
* - `pass-fail`: Full score only if every item is in its correct position.
|
|
170
|
+
* - `partial-position`: Credit for each item in its correct position.
|
|
171
|
+
* - `partial-sequence`: Credit for each pair of adjacent items in the correct order.
|
|
172
|
+
*/
|
|
173
|
+
@property({ type: String, attribute: "grading-method", reflect: true })
|
|
174
|
+
accessor gradingMethod: OrderGradingMethod = "pass-fail";
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The IDs of the items in their correct order. It is obfuscated in the markup so that learners cannot read it directly.
|
|
178
|
+
*/
|
|
179
|
+
@property({ type: Array, attribute: true, reflect: true, converter: encryptedProperty })
|
|
180
|
+
accessor solution: OrderSolution = [];
|
|
181
|
+
|
|
182
|
+
/** @internal */
|
|
183
|
+
@queryAssignedElements()
|
|
184
|
+
accessor items!: WebwriterOrderItem[];
|
|
185
|
+
|
|
186
|
+
protected updated(changed: PropertyValues): void {
|
|
187
|
+
super.updated(changed);
|
|
188
|
+
|
|
189
|
+
if (changed.has("layout") || changed.has("numberingStyle") || changed.has("direction")) {
|
|
190
|
+
this.refreshItems();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private randomizeItems() {
|
|
195
|
+
this.append(...randomizeArray(this.items));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
checkValidity(): boolean {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
reset(): void {
|
|
203
|
+
this.randomizeItems();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
checkAnswer(detailedFeedback: boolean): number {
|
|
207
|
+
// TODO: Show detailed feedback if requested
|
|
208
|
+
|
|
209
|
+
switch (this.gradingMethod) {
|
|
210
|
+
case "pass-fail":
|
|
211
|
+
return this.items.every((item, index) => item.id === this.solution[index]) ? 1 : 0;
|
|
212
|
+
case "partial-position":
|
|
213
|
+
const correctPositions = this.items.filter((item, index) => item.id === this.solution[index]).length;
|
|
214
|
+
return correctPositions / this.items.length;
|
|
215
|
+
case "partial-sequence":
|
|
216
|
+
let correctSequenceCount = 0;
|
|
217
|
+
for (let i = 0; i < this.items.length - 1; i++) {
|
|
218
|
+
const currentIndexInSolution = this.solution.indexOf(this.items[i].id);
|
|
219
|
+
const nextIndexInSolution = this.solution.indexOf(this.items[i + 1].id);
|
|
220
|
+
|
|
221
|
+
if (currentIndexInSolution + 1 === nextIndexInSolution) correctSequenceCount++;
|
|
222
|
+
}
|
|
223
|
+
return correctSequenceCount / (this.items.length - 1);
|
|
224
|
+
default:
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected firstUpdated(changed: PropertyValues): void {
|
|
230
|
+
super.firstUpdated(changed);
|
|
231
|
+
encryptPlaintextAttributes(this);
|
|
232
|
+
if (!this.isContentEditable) {
|
|
233
|
+
this.randomizeItems();
|
|
234
|
+
} else {
|
|
235
|
+
this.solution = this.items.map(item => item.id);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private refreshItems() {
|
|
240
|
+
this.items.forEach(item => item?.requestUpdate?.());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
connectedCallback() {
|
|
244
|
+
super.connectedCallback();
|
|
245
|
+
registerAsEnterCreatesNew("webwriter-order-item");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private handleReorder({ detail }: DraggableGridReorderEvent) {
|
|
249
|
+
mutateWithStableSelection(detail.item, () => this.insertBefore(detail.item, detail.before));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@action({ label: { _: msg("Add Option") } })
|
|
253
|
+
private addOption() {
|
|
254
|
+
this.appendChild(document.createElement("webwriter-order-item"));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private AddOptionButton() {
|
|
258
|
+
if (this.layout !== "tiles" || !this.isContentEditable) return nothing;
|
|
259
|
+
return html`
|
|
260
|
+
<div class="add-option" @click=${this.addOption} id="add-option" data-not-draggable>
|
|
261
|
+
<sl-icon src=${PlusIcon}></sl-icon>
|
|
262
|
+
${msg("Add Option")}
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
render() {
|
|
268
|
+
return html`<draggable-grid
|
|
269
|
+
class="container-${this.layout}"
|
|
270
|
+
?vertical=${this.layout === "list"}
|
|
271
|
+
@reorder=${this.handleReorder}
|
|
272
|
+
>
|
|
273
|
+
<slot
|
|
274
|
+
@slotchange=${() => {
|
|
275
|
+
this.refreshItems();
|
|
276
|
+
if (this.isContentEditable) this.solution = this.items.map(item => item.id);
|
|
277
|
+
}}
|
|
278
|
+
></slot>
|
|
279
|
+
${this.AddOptionButton()}
|
|
280
|
+
</draggable-grid>`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single item of a `<webwriter-pairing>`.
|
|
9
|
+
*
|
|
10
|
+
* It must be a child of its parent element.
|
|
11
|
+
*/
|
|
12
|
+
@customElement("webwriter-pairing-item")
|
|
13
|
+
export class WebwriterPairing extends LitElementWw {
|
|
14
|
+
static styles = css`
|
|
15
|
+
:host {
|
|
16
|
+
display: block;
|
|
17
|
+
aspect-ratio: 1 / 1;
|
|
18
|
+
width: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
slot {
|
|
24
|
+
display: grid;
|
|
25
|
+
align-items: center;
|
|
26
|
+
text-align: center;
|
|
27
|
+
height: 100%;
|
|
28
|
+
|
|
29
|
+
padding: var(--sl-spacing-x-small);
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
::slotted(p) {
|
|
34
|
+
min-width: 0;
|
|
35
|
+
overflow-wrap: anywhere;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
return html`<slot
|
|
41
|
+
style=${styleMap({
|
|
42
|
+
"--ww-placeholder": `"${msg("Item")}"`,
|
|
43
|
+
})}
|
|
44
|
+
></slot>`;
|
|
45
|
+
}
|
|
46
|
+
}
|