@webwriter/quiz 1.0.3 → 1.0.4

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 (41) hide show
  1. package/LICENSE +6 -6
  2. package/custom.d.ts +175 -175
  3. package/dist/widgets/webwriter-choice-item.js +58 -13
  4. package/dist/widgets/webwriter-choice.js +64 -19
  5. package/dist/widgets/webwriter-cloze-gap.js +59 -14
  6. package/dist/widgets/webwriter-cloze.js +56 -11
  7. package/dist/widgets/webwriter-mark.js +56 -11
  8. package/dist/widgets/webwriter-order-item.js +56 -11
  9. package/dist/widgets/webwriter-order.js +64 -19
  10. package/dist/widgets/webwriter-pairing.js +59 -14
  11. package/dist/widgets/webwriter-quiz.js +67 -22
  12. package/dist/widgets/webwriter-speech.js +66 -20
  13. package/dist/widgets/webwriter-task.js +534 -308
  14. package/dist/widgets/webwriter-text.js +60 -14
  15. package/package.json +200 -200
  16. package/src/lib/combobox.ts +455 -455
  17. package/src/snippets/choice.html +7 -7
  18. package/src/snippets/cloze.html +3 -3
  19. package/src/snippets/mark.html +3 -3
  20. package/src/snippets/order.html +7 -7
  21. package/src/snippets/pairing.html +9 -9
  22. package/src/snippets/speech.html +3 -3
  23. package/src/snippets/text.html +3 -3
  24. package/src/snippets/wordsearch.html +3 -3
  25. package/src/widgets/webwriter-choice-item.ts +250 -250
  26. package/src/widgets/webwriter-choice.ts +309 -309
  27. package/src/widgets/webwriter-cloze-gap.ts +216 -216
  28. package/src/widgets/webwriter-cloze.ts +135 -135
  29. package/src/widgets/webwriter-mark.ts +434 -434
  30. package/src/widgets/webwriter-order-item.ts +444 -444
  31. package/src/widgets/webwriter-order.ts +272 -272
  32. package/src/widgets/webwriter-pairing-item.ts +155 -155
  33. package/src/widgets/webwriter-pairing.ts +187 -187
  34. package/src/widgets/webwriter-quiz.ts +280 -280
  35. package/src/widgets/webwriter-speech.ts +267 -267
  36. package/src/widgets/webwriter-task-explainer.ts +37 -37
  37. package/src/widgets/webwriter-task-hint.ts +16 -16
  38. package/src/widgets/webwriter-task-prompt.ts +17 -17
  39. package/src/widgets/webwriter-task.ts +543 -543
  40. package/src/widgets/webwriter-text.ts +130 -130
  41. package/tsconfig.json +6 -6
@@ -1,310 +1,310 @@
1
- import {html, css, PropertyValueMap, TemplateResult, PropertyValues} from "lit"
2
- import {styleMap} from "lit/directives/style-map.js"
3
- import {LitElementWw, option, action} from "@webwriter/lit"
4
- import {customElement, property, queryAssignedElements, query} from "lit/decorators.js"
5
-
6
- import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"
7
- import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
8
- import SlRadio from "@shoelace-style/shoelace/dist/components/radio/radio.component.js"
9
- import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js"
10
- import SlRadioButton from "@shoelace-style/shoelace/dist/components/radio-button/radio-button.component.js"
11
- import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.component.js"
12
-
13
- import IconPlusSquare from "bootstrap-icons/icons/plus-square.svg"
14
- import IconPlusCircle from "bootstrap-icons/icons/plus-circle.svg"
15
- import { WebwriterChoiceItem } from "./webwriter-choice-item.js"
16
- import "@shoelace-style/shoelace/dist/themes/light.css"
17
- import MiniMasonry from "minimasonry"
18
-
19
-
20
- export function shuffle<T>(a: T[]) {
21
- for (let i = a.length - 1; i > 0; i--) {
22
- const j = Math.floor(Math.random() * (i + 1));
23
- [a[i], a[j]] = [a[j], a[i]];
24
- }
25
- return a;
26
- }
27
-
28
- declare global {interface HTMLElementTagNameMap {
29
- "webwriter-choice": WebwriterChoice;
30
- }}
31
-
32
- @customElement("webwriter-choice")
33
- export class WebwriterChoice extends LitElementWw {
34
-
35
- @property({type: String, attribute: true, reflect: true})
36
- @option({
37
- type: "select",
38
- options: [
39
- {value: "truefalse", label: {"en": "True/False"}},
40
- {value: "single", label: {"en": "Single Choice"}},
41
- {value: "multiple", label: {"en": "Multiple Choice"}},
42
- ]
43
- })
44
- accessor mode: "truefalse" | "single" | "multiple" = "single"
45
-
46
-
47
- get layout(): "list" | "tiles" {
48
- return this.children?.item(0)?.getAttribute("layout") as any ?? "list"
49
- }
50
-
51
- @property({type: String, attribute: true, reflect: true}) //@ts-ignore
52
- @option({
53
- type: "select",
54
- options: [
55
- {value: "list", label: {"en": "List"}},
56
- {value: "tiles", label: {"en": "Tiles"}}
57
- ],
58
- })
59
- set layout(value) {
60
- this.querySelectorAll("webwriter-choice-item").forEach(el => el.setAttribute("layout", value))
61
- this.requestUpdate("layout")
62
- }
63
-
64
- @property({type: Boolean, attribute: true, reflect: true})
65
- @option({
66
- type: Boolean,
67
- label: {"en": "Random Choice Order"}
68
- })
69
- accessor randomOrder = false
70
-
71
-
72
- get solution() {
73
- const validIDs = this.items.filter(item => item.valid).map(item => item.id)
74
- return validIDs.length? validIDs: undefined
75
- }
76
-
77
- set solution(value: string[]) {
78
- this.items.forEach(item => value.includes(item.id)? item.valid=true: null)
79
- }
80
-
81
-
82
- reportSolution() {
83
- if(!this.solution) {
84
- this.items.forEach(item => item.valid = true)
85
- return
86
- }
87
- this.items.forEach(item => item.valid = (this.solution ?? []).includes(item.id))
88
- }
89
-
90
- reset() {
91
- this.items.forEach(item => {item.valid = undefined; item.active = false})
92
- this.shuffleItems()
93
- }
94
-
95
- static scopedElements = {
96
- "sl-button": SlButton,
97
- "sl-icon": SlIcon,
98
- "sl-radio": SlRadio,
99
- "sl-checkbox": SlCheckbox,
100
- "sl-radio-button": SlRadioButton,
101
- "sl-radio-group": SlRadioGroup
102
- }
103
-
104
- @query("slot")
105
- accessor slotEl: HTMLSlotElement
106
-
107
- @action()
108
- addItem() {
109
- /*
110
- if(this.ownerDocument.getSelection().containsNode(this, true)) {
111
- this.ownerDocument.getSelection().modify("move", "backward", "character")
112
- }
113
- setTimeout(() => {*/
114
- const choiceItem = this.ownerDocument.createElement("webwriter-choice-item")
115
- const p = this.ownerDocument.createElement("p")
116
- choiceItem.appendChild(p)
117
- choiceItem.setAttribute("layout", this.layout)
118
- this.appendChild(choiceItem)
119
- this.ownerDocument.getSelection().setBaseAndExtent(p, 0, p, 0)
120
- //})
121
- }
122
-
123
- shuffleItems() {
124
- const items = this.slotEl.assignedElements() as WebwriterChoiceItem[]
125
- const n = items.length
126
- const nums = shuffle([...(new Array(n)).keys()])
127
- items.forEach((el, i) => el.style.order = String(nums[i]))
128
- }
129
-
130
- connectedCallback(): void {
131
- super.connectedCallback()
132
- const observer = new MutationObserver(() => {
133
- if(!this.contentEditable && this.randomOrder) {
134
- this.shuffleItems()
135
- }
136
- })
137
- observer.observe(this, {childList: true})
138
- }
139
-
140
- protected firstUpdated(_changedProperties: PropertyValues): void {
141
- if(!this.contentEditable && this.randomOrder) {
142
- this.shuffleItems()
143
- }
144
- }
145
-
146
- static styles = css`
147
- :host {
148
- display: flex !important;
149
- flex-direction: column;
150
- align-items: flex-start;
151
- gap: 0.5rem;
152
- }
153
-
154
- :host([layout=tiles]) {
155
-
156
- flex-direction: row;
157
- flex-wrap: wrap;
158
- gap: 15px;
159
-
160
- & ::slotted(*) {
161
- aspect-ratio: 1;
162
- min-width: 125px;
163
- width: 125px;
164
- max-width: 350px;
165
- min-height: 125px;
166
- height: 125px;
167
- max-height: 350px;
168
- overflow: hidden;
169
- resize: both;
170
- border: 2px solid var(--sl-color-gray-500);
171
- border-radius: 5px;
172
- }
173
- }
174
-
175
- sl-button::part(label) {
176
- padding: 0;
177
- display: flex;
178
- flex-direction: row;
179
- align-items: center;
180
- }
181
-
182
- sl-button::part(base) {
183
- border: none;
184
- background: transparent;
185
- }
186
-
187
- sl-icon {
188
- width: 19px;
189
- height: 19px;
190
- }
191
-
192
- sl-icon::part(svg) {
193
- overflow: visible;
194
- }
195
-
196
- :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
197
- display: none !important;
198
- }
199
-
200
- #add-option span {
201
- margin-inline-start: 0.5em;
202
- }
203
-
204
- :host(:is([contenteditable=true], [contenteditable=""])) ::slotted(*) {
205
- order: unset !important;
206
- }
207
-
208
- :host(:not([mode=multiple])) {
209
- --webwriter-choice-radius: 100%;
210
- }
211
-
212
- #add-option:not(:hover)::part(base) {
213
- color: darkgray;
214
- }
215
-
216
- :host([layout=tiles]) #add-option {
217
- width: 125px;
218
- height: 125px;
219
- overflow: hidden;
220
- border: 2px solid var(--sl-color-gray-300);
221
- border-radius: 5px;
222
- padding: 5px;
223
- display: flex;
224
- align-items: center;
225
- justify-content: center;
226
- }
227
-
228
- :host([mode=truefalse]) #add-option {
229
- display: none;
230
- }
231
- `
232
-
233
- @queryAssignedElements()
234
- accessor items: WebwriterChoiceItem[]
235
-
236
- #value: number[] = []
237
-
238
- get value() {
239
- return this.#value
240
- }
241
-
242
- @property({type: Array, attribute: false})
243
- set value(value: number[]) {
244
- this.#value = value
245
- this.items.forEach((item, i) => {
246
- this.isContentEditable
247
- ? item.valid = this.#value.includes(i)
248
- : item.active = this.#value.includes(i)
249
- })
250
- }
251
-
252
- private applyValue() {
253
- this.#value = this.items
254
- .map((item, i) => [item.valid, i] as [boolean, number])
255
- .filter(([valid]) => valid)
256
- .map(([_, i]) => i)
257
- }
258
-
259
- handleControlChange = (e: CustomEvent) => {
260
- if(this.mode === "single") {
261
- const item = e.target as WebwriterChoiceItem
262
- const otherItems = this.items.filter(el => el !== item)
263
- otherItems.forEach(el => this.isContentEditable? el.valid = false: el.active = false)
264
- }
265
- this.applyValue()
266
- this.dispatchEvent(new CustomEvent("ww-answer-change", {
267
- detail: {value: this.value},
268
- bubbles: true,
269
- composed: true
270
- }))
271
- }
272
-
273
- handleSlotChange(e: Event) {
274
- this.requestUpdate()
275
- }
276
-
277
- protected willUpdate(changed: PropertyValueMap<any>) {
278
- if(changed.has("mode") && this.mode === "single") {
279
- this.items
280
- .filter(el => this.isContentEditable? el.valid: el.active)
281
- .slice(1)
282
- .forEach(el => this.isContentEditable? el.valid = false: el.active = false)
283
- }
284
- if(changed.has("mode") && this.mode === "truefalse") {
285
- this.items.forEach(el => el.remove())
286
- }
287
- }
288
-
289
- @query("#items-slot")
290
- accessor itemsSlotEl: HTMLSlotElement
291
-
292
- render() {
293
- return html`
294
- ${this.mode === "truefalse"
295
- ? html`
296
- <sl-radio-group>
297
- <sl-radio-button value="true">True</sl-radio-button>
298
- <sl-radio-button value="false">False</sl-radio-button>
299
- </sl-radio-group>
300
- `
301
- : html`
302
- <slot id="items-slot" @sl-change=${this.handleControlChange} @slotchange=${this.handleSlotChange}></slot>
303
- `
304
- }
305
- <sl-button size="small" id="add-option" class="author-only" @click=${() => this.addItem()}>
306
- <sl-icon src=${this.mode === "multiple"? IconPlusSquare: IconPlusCircle}></sl-icon><span>Add Option</span>
307
- </sl-button>
308
- `
309
- }
1
+ import {html, css, PropertyValueMap, TemplateResult, PropertyValues} from "lit"
2
+ import {styleMap} from "lit/directives/style-map.js"
3
+ import {LitElementWw, option, action} from "@webwriter/lit"
4
+ import {customElement, property, queryAssignedElements, query} from "lit/decorators.js"
5
+
6
+ import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"
7
+ import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
8
+ import SlRadio from "@shoelace-style/shoelace/dist/components/radio/radio.component.js"
9
+ import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js"
10
+ import SlRadioButton from "@shoelace-style/shoelace/dist/components/radio-button/radio-button.component.js"
11
+ import SlRadioGroup from "@shoelace-style/shoelace/dist/components/radio-group/radio-group.component.js"
12
+
13
+ import IconPlusSquare from "bootstrap-icons/icons/plus-square.svg"
14
+ import IconPlusCircle from "bootstrap-icons/icons/plus-circle.svg"
15
+ import type { WebwriterChoiceItem } from "./webwriter-choice-item.js"
16
+ import "@shoelace-style/shoelace/dist/themes/light.css"
17
+ import MiniMasonry from "minimasonry"
18
+
19
+
20
+ export function shuffle<T>(a: T[]) {
21
+ for (let i = a.length - 1; i > 0; i--) {
22
+ const j = Math.floor(Math.random() * (i + 1));
23
+ [a[i], a[j]] = [a[j], a[i]];
24
+ }
25
+ return a;
26
+ }
27
+
28
+ declare global {interface HTMLElementTagNameMap {
29
+ "webwriter-choice": WebwriterChoice;
30
+ }}
31
+
32
+ @customElement("webwriter-choice")
33
+ export class WebwriterChoice extends LitElementWw {
34
+
35
+ @property({type: String, attribute: true, reflect: true})
36
+ @option({
37
+ type: "select",
38
+ options: [
39
+ {value: "truefalse", label: {"en": "True/False"}},
40
+ {value: "single", label: {"en": "Single Choice"}},
41
+ {value: "multiple", label: {"en": "Multiple Choice"}},
42
+ ]
43
+ })
44
+ accessor mode: "truefalse" | "single" | "multiple" = "single"
45
+
46
+
47
+ get layout(): "list" | "tiles" {
48
+ return this.children?.item(0)?.getAttribute("layout") as any ?? "list"
49
+ }
50
+
51
+ @property({type: String, attribute: true, reflect: true}) //@ts-ignore
52
+ @option({
53
+ type: "select",
54
+ options: [
55
+ {value: "list", label: {"en": "List"}},
56
+ {value: "tiles", label: {"en": "Tiles"}}
57
+ ],
58
+ })
59
+ set layout(value) {
60
+ this.querySelectorAll("webwriter-choice-item").forEach(el => el.setAttribute("layout", value))
61
+ this.requestUpdate("layout")
62
+ }
63
+
64
+ @property({type: Boolean, attribute: true, reflect: true})
65
+ @option({
66
+ type: Boolean,
67
+ label: {"en": "Random Choice Order"}
68
+ })
69
+ accessor randomOrder = false
70
+
71
+
72
+ get solution() {
73
+ const validIDs = this.items.filter(item => item.valid).map(item => item.id)
74
+ return validIDs.length? validIDs: undefined
75
+ }
76
+
77
+ set solution(value: string[]) {
78
+ this.items.forEach(item => value.includes(item.id)? item.valid=true: null)
79
+ }
80
+
81
+
82
+ reportSolution() {
83
+ if(!this.solution) {
84
+ this.items.forEach(item => item.valid = true)
85
+ return
86
+ }
87
+ this.items.forEach(item => item.valid = (this.solution ?? []).includes(item.id))
88
+ }
89
+
90
+ reset() {
91
+ this.items.forEach(item => {item.valid = undefined; item.active = false})
92
+ this.shuffleItems()
93
+ }
94
+
95
+ static scopedElements = {
96
+ "sl-button": SlButton,
97
+ "sl-icon": SlIcon,
98
+ "sl-radio": SlRadio,
99
+ "sl-checkbox": SlCheckbox,
100
+ "sl-radio-button": SlRadioButton,
101
+ "sl-radio-group": SlRadioGroup
102
+ }
103
+
104
+ @query("slot")
105
+ accessor slotEl: HTMLSlotElement
106
+
107
+ @action()
108
+ addItem() {
109
+ /*
110
+ if(this.ownerDocument.getSelection().containsNode(this, true)) {
111
+ this.ownerDocument.getSelection().modify("move", "backward", "character")
112
+ }
113
+ setTimeout(() => {*/
114
+ const choiceItem = this.ownerDocument.createElement("webwriter-choice-item")
115
+ const p = this.ownerDocument.createElement("p")
116
+ choiceItem.appendChild(p)
117
+ choiceItem.setAttribute("layout", this.layout)
118
+ this.appendChild(choiceItem)
119
+ this.ownerDocument.getSelection().setBaseAndExtent(p, 0, p, 0)
120
+ //})
121
+ }
122
+
123
+ shuffleItems() {
124
+ const items = this.slotEl.assignedElements() as WebwriterChoiceItem[]
125
+ const n = items.length
126
+ const nums = shuffle([...(new Array(n)).keys()])
127
+ items.forEach((el, i) => el.style.order = String(nums[i]))
128
+ }
129
+
130
+ connectedCallback(): void {
131
+ super.connectedCallback()
132
+ const observer = new MutationObserver(() => {
133
+ if(!this.contentEditable && this.randomOrder) {
134
+ this.shuffleItems()
135
+ }
136
+ })
137
+ observer.observe(this, {childList: true})
138
+ }
139
+
140
+ protected firstUpdated(_changedProperties: PropertyValues): void {
141
+ if(!this.contentEditable && this.randomOrder) {
142
+ this.shuffleItems()
143
+ }
144
+ }
145
+
146
+ static styles = css`
147
+ :host {
148
+ display: flex !important;
149
+ flex-direction: column;
150
+ align-items: flex-start;
151
+ gap: 0.5rem;
152
+ }
153
+
154
+ :host([layout=tiles]) {
155
+
156
+ flex-direction: row;
157
+ flex-wrap: wrap;
158
+ gap: 15px;
159
+
160
+ & ::slotted(*) {
161
+ aspect-ratio: 1;
162
+ min-width: 125px;
163
+ width: 125px;
164
+ max-width: 350px;
165
+ min-height: 125px;
166
+ height: 125px;
167
+ max-height: 350px;
168
+ overflow: hidden;
169
+ resize: both;
170
+ border: 2px solid var(--sl-color-gray-500);
171
+ border-radius: 5px;
172
+ }
173
+ }
174
+
175
+ sl-button::part(label) {
176
+ padding: 0;
177
+ display: flex;
178
+ flex-direction: row;
179
+ align-items: center;
180
+ }
181
+
182
+ sl-button::part(base) {
183
+ border: none;
184
+ background: transparent;
185
+ }
186
+
187
+ sl-icon {
188
+ width: 19px;
189
+ height: 19px;
190
+ }
191
+
192
+ sl-icon::part(svg) {
193
+ overflow: visible;
194
+ }
195
+
196
+ :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
197
+ display: none !important;
198
+ }
199
+
200
+ #add-option span {
201
+ margin-inline-start: 0.5em;
202
+ }
203
+
204
+ :host(:is([contenteditable=true], [contenteditable=""])) ::slotted(*) {
205
+ order: unset !important;
206
+ }
207
+
208
+ :host(:not([mode=multiple])) {
209
+ --webwriter-choice-radius: 100%;
210
+ }
211
+
212
+ #add-option:not(:hover)::part(base) {
213
+ color: darkgray;
214
+ }
215
+
216
+ :host([layout=tiles]) #add-option {
217
+ width: 125px;
218
+ height: 125px;
219
+ overflow: hidden;
220
+ border: 2px solid var(--sl-color-gray-300);
221
+ border-radius: 5px;
222
+ padding: 5px;
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ }
227
+
228
+ :host([mode=truefalse]) #add-option {
229
+ display: none;
230
+ }
231
+ `
232
+
233
+ @queryAssignedElements()
234
+ accessor items: WebwriterChoiceItem[]
235
+
236
+ #value: number[] = []
237
+
238
+ get value() {
239
+ return this.#value
240
+ }
241
+
242
+ @property({type: Array, attribute: false})
243
+ set value(value: number[]) {
244
+ this.#value = value
245
+ this.items.forEach((item, i) => {
246
+ this.isContentEditable
247
+ ? item.valid = this.#value.includes(i)
248
+ : item.active = this.#value.includes(i)
249
+ })
250
+ }
251
+
252
+ private applyValue() {
253
+ this.#value = this.items
254
+ .map((item, i) => [item.valid, i] as [boolean, number])
255
+ .filter(([valid]) => valid)
256
+ .map(([_, i]) => i)
257
+ }
258
+
259
+ handleControlChange = (e: CustomEvent) => {
260
+ if(this.mode === "single") {
261
+ const item = e.target as WebwriterChoiceItem
262
+ const otherItems = this.items.filter(el => el !== item)
263
+ otherItems.forEach(el => this.isContentEditable? el.valid = false: el.active = false)
264
+ }
265
+ this.applyValue()
266
+ this.dispatchEvent(new CustomEvent("ww-answer-change", {
267
+ detail: {value: this.value},
268
+ bubbles: true,
269
+ composed: true
270
+ }))
271
+ }
272
+
273
+ handleSlotChange(e: Event) {
274
+ this.requestUpdate()
275
+ }
276
+
277
+ protected willUpdate(changed: PropertyValueMap<any>) {
278
+ if(changed.has("mode") && this.mode === "single") {
279
+ this.items
280
+ .filter(el => this.isContentEditable? el.valid: el.active)
281
+ .slice(1)
282
+ .forEach(el => this.isContentEditable? el.valid = false: el.active = false)
283
+ }
284
+ if(changed.has("mode") && this.mode === "truefalse") {
285
+ this.items.forEach(el => el.remove())
286
+ }
287
+ }
288
+
289
+ @query("#items-slot")
290
+ accessor itemsSlotEl: HTMLSlotElement
291
+
292
+ render() {
293
+ return html`
294
+ ${this.mode === "truefalse"
295
+ ? html`
296
+ <sl-radio-group>
297
+ <sl-radio-button value="true">True</sl-radio-button>
298
+ <sl-radio-button value="false">False</sl-radio-button>
299
+ </sl-radio-group>
300
+ `
301
+ : html`
302
+ <slot id="items-slot" @sl-change=${this.handleControlChange} @slotchange=${this.handleSlotChange}></slot>
303
+ `
304
+ }
305
+ <sl-button size="small" id="add-option" class="author-only" @click=${() => this.addItem()}>
306
+ <sl-icon src=${this.mode === "multiple"? IconPlusSquare: IconPlusCircle}></sl-icon><span>Add Option</span>
307
+ </sl-button>
308
+ `
309
+ }
310
310
  }