@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
@@ -1,254 +0,0 @@
1
- import {css, html, PropertyValues} from "lit"
2
- import {styleMap} from "lit/directives/style-map.js"
3
- import {classMap} from "lit/directives/class-map.js"
4
- import {unsafeStatic} from "lit/static-html.js"
5
- import {LitElementWw} from "@webwriter/lit"
6
- import {customElement, property, query} from "lit/decorators.js"
7
-
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 SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
11
- import "@shoelace-style/shoelace/dist/themes/light.css"
12
- import { keyed } from "lit/directives/keyed.js"
13
- import IconX from "bootstrap-icons/icons/x.svg"
14
- import IconCheck from "bootstrap-icons/icons/check.svg"
15
-
16
- import LOCALIZE from "../../localization/generated"
17
- import {msg} from "@lit/localize"
18
-
19
- declare global {interface HTMLElementTagNameMap {
20
- "webwriter-choice-item": WebwriterChoiceItem;
21
- }}
22
-
23
- @customElement("webwriter-choice-item")
24
- export class WebwriterChoiceItem extends LitElementWw {
25
-
26
- localize = LOCALIZE
27
-
28
- @property({type: Boolean, attribute: false})
29
- accessor showSolution = true
30
-
31
- @property({type: Boolean, attribute: false})
32
- accessor active = false
33
-
34
- @property({type: Boolean, attribute: false})
35
- accessor valid: true | false | undefined
36
-
37
- // inactive + invalid/no validity -> nothing
38
- // active + no validity -> blue circle
39
- // inactive/active + valid -> green upper checkmark
40
- // active + invalid -> red upper cross
41
-
42
-
43
- @property({type: String, attribute: true, reflect: true})
44
- accessor layout: "list" | "tiles" = "list"
45
-
46
- static scopedElements = {
47
- "sl-radio": SlRadio,
48
- "sl-checkbox": SlCheckbox,
49
- "sl-icon": SlIcon
50
- }
51
-
52
- static styles = css`
53
-
54
- :host {
55
- width: 100%;
56
- position: relative;
57
- }
58
-
59
- :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
60
- display: none;
61
- }
62
-
63
- :host(:is([contenteditable=true], [contenteditable=""])) .user-only {
64
- display: none;
65
- }
66
-
67
- :host(:is([contenteditable=true], [contenteditable=""])) sl-checkbox.valid {
68
- --webwriter-control-color-600: var(--sl-color-success-600);
69
- --webwriter-control-color-400: var(--sl-color-success-400);
70
- }
71
-
72
- sl-checkbox {
73
- display: block;
74
- width: 100%;
75
- &::part(base) {
76
- width: 100%;
77
- display: flex;
78
- flex-direction: row;
79
- align-items: center;
80
- cursor: unset;
81
- }
82
-
83
- &::part(control) {
84
- cursor: pointer;
85
- border-radius: var(--webwriter-choice-radius, 2px);
86
- border-width: 2px;
87
- }
88
-
89
- &::part(control):not(:hover){
90
- border-color: var(--sl-color-gray-500);
91
- }
92
-
93
- &::part(control):hover {
94
- border-color: var(--sl-color-gray-700);
95
- }
96
-
97
- &::part(control--checked):not(:hover) {
98
- background-color: var(--webwriter-control-color-600, var(--sl-color-primary-600));
99
- border-color: var(--sl-color-gray-500);
100
- }
101
- &::part(control--checked):hover {
102
- background-color: var(--webwriter-control-color-400, var(--sl-color-primary-400));
103
- border-color: var(--webwriter-control-color-600, var(--sl-color-primary-600));
104
- }
105
-
106
- &::part(label) {
107
- width: 100%;
108
- }
109
- }
110
-
111
- .solution {
112
- position: absolute;
113
- top: -6px;
114
- left: -6px;
115
- border: 2px solid var(--sl-color-gray-500);
116
- border-radius: var(--webwriter-choice-radius, 2px);
117
- width: 12px;
118
- height: 12px;
119
- z-index: 1;
120
- color: white;
121
-
122
- &[data-valid] {
123
- background: var(--sl-color-success-600);
124
- }
125
-
126
- &:not([data-valid]) {
127
- background: var(--sl-color-danger-600);
128
- }
129
- }
130
-
131
- :host([layout=tiles]) {
132
- position: relative;
133
- overflow: visible !important;
134
-
135
- & ::slotted(:is(picture, audio, video, img, iframe)) {
136
- height: 100%;
137
- width: 100%;
138
- }
139
-
140
- & ::slotted(:not(:is(picture, audio, video, img, iframe))) {
141
- margin: 5px !important;
142
- }
143
-
144
- sl-checkbox {
145
- display: block;
146
- &::part(base) {
147
- display: flex;
148
- flex-direction: row;
149
- align-items: center;
150
- cursor: unset;
151
- position: static;
152
- }
153
-
154
- &::part(control) {
155
- position: absolute;
156
- bottom: -10px;
157
- left: -10px;
158
- cursor: pointer;
159
- border-radius: var(--webwriter-choice-radius, 2px);
160
- z-index: 100;
161
- border-color: var(--sl-color-gray-500);
162
- }
163
-
164
- &::part(control--checked):not(:hover) {
165
- background-color: var(--webwriter-control-color-600, var(--sl-color-primary-600));
166
- border-color: var(--sl-color-gray-500);
167
- }
168
- &::part(control--checked):hover {
169
- background-color: var(--webwriter-control-color-400, var(--sl-color-primary-400));
170
- border-color: var(--webwriter-control-color-600, var(--sl-color-primary-600));
171
- }
172
-
173
- &::part(label) {
174
- aspect-ratio: 1;
175
- min-width: 125px;
176
- max-width: 350px;
177
- min-height: 125px;
178
- max-height: 350px;
179
- overflow: hidden;
180
- resize: both;
181
- margin-inline-start: 0;
182
- overflow-y: auto;
183
- scrollbar-width: thin;
184
- }
185
- }
186
- }
187
- `
188
-
189
- handleClick = (e: PointerEvent) => {
190
- const checkboxClicked = e.composedPath().some(v => (v as HTMLElement)?.classList?.contains("checkbox__control"))
191
- const editable = this.isContentEditable
192
- if(editable && !checkboxClicked) {
193
- e.preventDefault()
194
- }
195
- else {
196
- e.stopImmediatePropagation()
197
- }
198
- }
199
-
200
- handleChange = (e: CustomEvent) => {
201
- if(this.isContentEditable) {
202
- this.valid = !this.valid
203
- }
204
- else {
205
- this.active = !this.active
206
- }
207
- }
208
-
209
- @query("sl-checkbox")
210
- accessor checkbox: SlCheckbox
211
-
212
- observer: MutationObserver
213
-
214
- protected async updated(_changedProperties: PropertyValues) {
215
- await this.checkbox.updateComplete
216
- const labelEl = this.checkbox.shadowRoot.querySelector(".checkbox__label") as HTMLElement
217
-
218
- if(_changedProperties.has("layout") && this.layout === "list") {
219
- this.syncSize(true)
220
- this.observer?.disconnect()
221
- }
222
- else if(_changedProperties.has("layout") && this.layout === "tiles") {
223
- this.syncSize()
224
- this.observer = new MutationObserver(() => this.syncSize())
225
- this.observer.observe(labelEl, {attributeFilter: ["style"], attributes: true})
226
- }
227
- }
228
-
229
- syncSize(clear=false) {
230
- const labelEl = this.checkbox?.shadowRoot.querySelector(".checkbox__label") as HTMLElement
231
- if(labelEl && !clear) {
232
- this.style.width = labelEl.style.width
233
- this.style.height = labelEl.style.height
234
- }
235
- else if(labelEl && clear) {
236
- this.style.width = labelEl.style.width = null
237
- this.style.height = labelEl.style.height = null
238
- }
239
- }
240
-
241
- disconnectedCallback(): void {
242
- super.disconnectedCallback()
243
- this.observer?.disconnect()
244
- }
245
-
246
- render() {
247
- return keyed(this.layout, html`
248
- ${this.showSolution && this.valid !== undefined && (this.active || !this.active && this.valid)? html`<sl-icon class="solution user-only" ?data-valid=${this.valid} src=${this.valid? IconCheck: IconX}></sl-icon>`: null}
249
- <sl-checkbox class=${classMap({valid: this.valid, active: this.active})} exportparts="base, control, label" @click=${this.handleClick} @sl-change=${this.handleChange} ?checked=${this.isContentEditable? this.valid: this.active}>
250
- <slot part="slot" style=${styleMap({"--ww-placeholder": `"${msg("Option")}"`})}></slot>
251
- </sl-checkbox>
252
- `)
253
- }
254
- }
@@ -1,353 +0,0 @@
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
- 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
- }
353
- }