@webwriter/quiz 1.1.5 → 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 (165) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +778 -0
  3. package/assets/icons/grip-horizontal.svg +8 -0
  4. package/assets/icons/grip-vertical.svg +8 -0
  5. package/assets/icons/truefalse.svg +6 -0
  6. package/custom-elements.json +2779 -0
  7. package/custom.d.ts +5 -1
  8. package/dist/api/index.d.ts +27 -0
  9. package/dist/api/index.js +40 -0
  10. package/dist/migrate.js +1434 -0
  11. package/dist/widgets/webwriter-choice-item.js +4095 -3818
  12. package/dist/widgets/webwriter-choice.js +2179 -5598
  13. package/dist/widgets/webwriter-gap-item.js +3987 -0
  14. package/dist/widgets/webwriter-gap.js +2908 -0
  15. package/dist/widgets/webwriter-mark.js +4347 -3368
  16. package/dist/widgets/webwriter-order-item.js +4038 -3197
  17. package/dist/widgets/webwriter-order.js +2885 -4200
  18. package/dist/widgets/webwriter-pairing-item.js +1070 -2330
  19. package/dist/widgets/webwriter-pairing.js +2968 -4231
  20. package/dist/widgets/webwriter-quiz-hint.js +2469 -0
  21. package/dist/widgets/webwriter-quiz.js +3337 -4364
  22. package/dist/widgets/webwriter-speech.js +6100 -5120
  23. package/dist/widgets/webwriter-task-prompt.js +4787 -2068
  24. package/dist/widgets/webwriter-task.js +2264 -8157
  25. package/dist/widgets/webwriter-text.js +4075 -4132
  26. package/dist/widgets/webwriter-true-false.js +4674 -0
  27. package/editing-config.json +106 -0
  28. package/lit-localize.json +20 -14
  29. package/localization/de.xlf +395 -65
  30. package/localization/es.xlf +396 -61
  31. package/localization/fr.xlf +396 -61
  32. package/localization/generated/de.ts +97 -15
  33. package/localization/generated/es.ts +97 -15
  34. package/localization/generated/fr.ts +98 -16
  35. package/localization/generated/index.d.ts +5 -0
  36. package/localization/generated/it.ts +96 -14
  37. package/localization/generated/locale-codes.js +0 -48
  38. package/localization/generated/nl.ts +99 -17
  39. package/localization/it.xlf +396 -61
  40. package/localization/nl.xlf +396 -61
  41. package/package.json +120 -254
  42. package/scripts/apply-translations.mjs +97 -0
  43. package/src/api/index.ts +72 -0
  44. package/src/lib/artboard.ts +686 -0
  45. package/src/lib/draggable-grid.ts +432 -0
  46. package/src/lib/encrypted-property.ts +202 -0
  47. package/src/lib/hint-popup.ts +152 -0
  48. package/src/lib/utils.ts +97 -0
  49. package/src/lib/webwriter-quirks.ts +121 -0
  50. package/src/migrate/index.ts +622 -0
  51. package/src/migrate/legacy-solution.ts +362 -0
  52. package/src/snippets/order.html +8 -7
  53. package/src/widgets/choice/webwriter-choice-item.ts +316 -0
  54. package/src/widgets/choice/webwriter-choice.ts +328 -0
  55. package/src/widgets/choice/webwriter-true-false.ts +113 -0
  56. package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
  57. package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
  58. package/src/widgets/dnd/webwriter-dnd.ts +36 -0
  59. package/src/widgets/gap/webwriter-gap-item.ts +250 -0
  60. package/src/widgets/gap/webwriter-gap.ts +447 -0
  61. package/src/widgets/order/webwriter-order-item.ts +243 -0
  62. package/src/widgets/order/webwriter-order.ts +282 -0
  63. package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
  64. package/src/widgets/pairing/webwriter-pairing.ts +934 -0
  65. package/src/widgets/speech/audio-player.ts +325 -0
  66. package/src/widgets/speech/audio-recorder.ts +285 -0
  67. package/src/widgets/speech/waveform.ts +113 -0
  68. package/src/widgets/speech/webwriter-speech.ts +87 -0
  69. package/src/widgets/webwriter-mark.ts +366 -424
  70. package/src/widgets/webwriter-quiz-hint.ts +36 -0
  71. package/src/widgets/webwriter-quiz.ts +318 -280
  72. package/src/widgets/webwriter-task-prompt.ts +43 -18
  73. package/src/widgets/webwriter-task.ts +212 -551
  74. package/src/widgets/webwriter-text.ts +286 -172
  75. package/tsconfig.api.json +12 -0
  76. package/tsconfig.json +7 -6
  77. package/dist/widgets/webwriter-choice.css +0 -444
  78. package/dist/widgets/webwriter-cloze-gap.css +0 -444
  79. package/dist/widgets/webwriter-cloze-gap.js +0 -5727
  80. package/dist/widgets/webwriter-cloze.css +0 -444
  81. package/dist/widgets/webwriter-cloze.js +0 -3653
  82. package/dist/widgets/webwriter-mark.css +0 -444
  83. package/dist/widgets/webwriter-order.css +0 -444
  84. package/dist/widgets/webwriter-pairing-item.css +0 -444
  85. package/dist/widgets/webwriter-pairing.css +0 -444
  86. package/dist/widgets/webwriter-quiz.css +0 -444
  87. package/dist/widgets/webwriter-task-explainer.css +0 -444
  88. package/dist/widgets/webwriter-task-explainer.js +0 -2769
  89. package/dist/widgets/webwriter-task-hint.css +0 -444
  90. package/dist/widgets/webwriter-task-hint.js +0 -2708
  91. package/dist/widgets/webwriter-task.css +0 -444
  92. package/dist/widgets/webwriter-text.css +0 -444
  93. package/localization/bg.xlf +0 -72
  94. package/localization/cs.xlf +0 -72
  95. package/localization/da.xlf +0 -72
  96. package/localization/el.xlf +0 -72
  97. package/localization/et.xlf +0 -72
  98. package/localization/fi.xlf +0 -72
  99. package/localization/generated/bg.ts +0 -31
  100. package/localization/generated/cs.ts +0 -31
  101. package/localization/generated/da.ts +0 -31
  102. package/localization/generated/el.ts +0 -31
  103. package/localization/generated/et.ts +0 -31
  104. package/localization/generated/fi.ts +0 -31
  105. package/localization/generated/hu.ts +0 -31
  106. package/localization/generated/id.ts +0 -31
  107. package/localization/generated/ja.ts +0 -31
  108. package/localization/generated/ko.ts +0 -31
  109. package/localization/generated/lt.ts +0 -31
  110. package/localization/generated/lv.ts +0 -31
  111. package/localization/generated/nb.ts +0 -31
  112. package/localization/generated/pl.ts +0 -31
  113. package/localization/generated/pt-BR.ts +0 -31
  114. package/localization/generated/pt-PT.ts +0 -31
  115. package/localization/generated/ro.ts +0 -31
  116. package/localization/generated/ru.ts +0 -31
  117. package/localization/generated/sk.ts +0 -31
  118. package/localization/generated/sl.ts +0 -31
  119. package/localization/generated/sv.ts +0 -31
  120. package/localization/generated/tr.ts +0 -31
  121. package/localization/generated/uk.ts +0 -31
  122. package/localization/generated/zh-hans.ts +0 -31
  123. package/localization/hu.xlf +0 -72
  124. package/localization/id.xlf +0 -72
  125. package/localization/ja.xlf +0 -72
  126. package/localization/ko.xlf +0 -72
  127. package/localization/lt.xlf +0 -72
  128. package/localization/lv.xlf +0 -72
  129. package/localization/nb.xlf +0 -72
  130. package/localization/pl.xlf +0 -72
  131. package/localization/pt-BR.xlf +0 -72
  132. package/localization/pt-PT.xlf +0 -72
  133. package/localization/ro.xlf +0 -72
  134. package/localization/ru.xlf +0 -72
  135. package/localization/sk.xlf +0 -72
  136. package/localization/sl.xlf +0 -72
  137. package/localization/sv.xlf +0 -72
  138. package/localization/tr.xlf +0 -72
  139. package/localization/uk.xlf +0 -72
  140. package/localization/zh-hans.xlf +0 -72
  141. package/src/lib/combobox.ts +0 -456
  142. package/src/lib/highlighter-fill.svg +0 -6
  143. package/src/snippets/Beispiel-Choice.html +0 -57
  144. package/src/snippets/Beispiel-Mark.html +0 -14
  145. package/src/snippets/Beispiel-Order.html +0 -52
  146. package/src/snippets/Beispiel-Text.html +0 -20
  147. package/src/snippets/blank.html +0 -1
  148. package/src/snippets/choice.html +0 -8
  149. package/src/snippets/cloze.html +0 -4
  150. package/src/snippets/mark.html +0 -4
  151. package/src/snippets/pairing.html +0 -10
  152. package/src/snippets/speech.html +0 -4
  153. package/src/snippets/text.html +0 -4
  154. package/src/snippets/wordsearch.html +0 -4
  155. package/src/widgets/webwriter-choice-item.ts +0 -257
  156. package/src/widgets/webwriter-choice.ts +0 -353
  157. package/src/widgets/webwriter-cloze-gap.ts +0 -216
  158. package/src/widgets/webwriter-cloze.ts +0 -136
  159. package/src/widgets/webwriter-order-item.ts +0 -441
  160. package/src/widgets/webwriter-order.ts +0 -327
  161. package/src/widgets/webwriter-pairing-item.ts +0 -161
  162. package/src/widgets/webwriter-pairing.ts +0 -193
  163. package/src/widgets/webwriter-speech.ts +0 -273
  164. package/src/widgets/webwriter-task-explainer.ts +0 -43
  165. package/src/widgets/webwriter-task-hint.ts +0 -23
@@ -1,257 +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
- margin-inline-start: 0;
181
- overflow-y: auto;
182
- scrollbar-width: thin;
183
- }
184
-
185
- :host(:is([contenteditable=true], [contenteditable=""])) &::part(label) {
186
- resize: both;
187
- }
188
- }
189
- }
190
- `
191
-
192
- handleClick = (e: PointerEvent) => {
193
- const checkboxClicked = e.composedPath().some(v => (v as HTMLElement)?.classList?.contains("checkbox__control"))
194
- const editable = this.isContentEditable
195
- if(editable && !checkboxClicked) {
196
- e.preventDefault()
197
- }
198
- else {
199
- e.stopImmediatePropagation()
200
- }
201
- }
202
-
203
- handleChange = (e: CustomEvent) => {
204
- if(this.isContentEditable) {
205
- this.valid = !this.valid
206
- }
207
- else {
208
- this.active = !this.active
209
- }
210
- }
211
-
212
- @query("sl-checkbox")
213
- accessor checkbox: SlCheckbox
214
-
215
- observer: MutationObserver
216
-
217
- protected async updated(_changedProperties: PropertyValues) {
218
- await this.checkbox.updateComplete
219
- const labelEl = this.checkbox.shadowRoot.querySelector(".checkbox__label") as HTMLElement
220
-
221
- if(_changedProperties.has("layout") && this.layout === "list") {
222
- this.syncSize(true)
223
- this.observer?.disconnect()
224
- }
225
- else if(_changedProperties.has("layout") && this.layout === "tiles") {
226
- this.syncSize()
227
- this.observer = new MutationObserver(() => this.syncSize())
228
- this.observer.observe(labelEl, {attributeFilter: ["style"], attributes: true})
229
- }
230
- }
231
-
232
- syncSize(clear=false) {
233
- const labelEl = this.checkbox?.shadowRoot.querySelector(".checkbox__label") as HTMLElement
234
- if(labelEl && !clear) {
235
- this.style.width = labelEl.style.width
236
- this.style.height = labelEl.style.height
237
- }
238
- else if(labelEl && clear) {
239
- this.style.width = labelEl.style.width = null
240
- this.style.height = labelEl.style.height = null
241
- }
242
- }
243
-
244
- disconnectedCallback(): void {
245
- super.disconnectedCallback()
246
- this.observer?.disconnect()
247
- }
248
-
249
- render() {
250
- return keyed(this.layout, html`
251
- ${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}
252
- <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}>
253
- <slot part="slot" style=${styleMap({"--ww-placeholder": `"${msg("Option")}"`})}></slot>
254
- </sl-checkbox>
255
- `)
256
- }
257
- }
@@ -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
- 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
- }
353
- }