@webwriter/quiz 1.1.2 → 1.1.5

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