@webwriter/quiz 1.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 (62) hide show
  1. package/LICENSE +7 -0
  2. package/custom.d.ts +176 -0
  3. package/dist/widgets/webwriter-choice-item.css +444 -0
  4. package/dist/widgets/webwriter-choice-item.js +288 -0
  5. package/dist/widgets/webwriter-choice.css +444 -0
  6. package/dist/widgets/webwriter-choice.js +327 -0
  7. package/dist/widgets/webwriter-cloze-gap.css +444 -0
  8. package/dist/widgets/webwriter-cloze-gap.js +722 -0
  9. package/dist/widgets/webwriter-cloze.css +444 -0
  10. package/dist/widgets/webwriter-cloze.js +177 -0
  11. package/dist/widgets/webwriter-mark.css +444 -0
  12. package/dist/widgets/webwriter-mark.js +410 -0
  13. package/dist/widgets/webwriter-order-item.css +444 -0
  14. package/dist/widgets/webwriter-order-item.js +464 -0
  15. package/dist/widgets/webwriter-order.css +444 -0
  16. package/dist/widgets/webwriter-order.js +560 -0
  17. package/dist/widgets/webwriter-pairing-item.css +444 -0
  18. package/dist/widgets/webwriter-pairing-item.js +203 -0
  19. package/dist/widgets/webwriter-pairing.css +444 -0
  20. package/dist/widgets/webwriter-pairing.js +224 -0
  21. package/dist/widgets/webwriter-quiz.css +444 -0
  22. package/dist/widgets/webwriter-quiz.js +323 -0
  23. package/dist/widgets/webwriter-speech.css +444 -0
  24. package/dist/widgets/webwriter-speech.js +320 -0
  25. package/dist/widgets/webwriter-task-explainer.css +444 -0
  26. package/dist/widgets/webwriter-task-explainer.js +90 -0
  27. package/dist/widgets/webwriter-task-hint.css +444 -0
  28. package/dist/widgets/webwriter-task-hint.js +67 -0
  29. package/dist/widgets/webwriter-task-prompt.css +444 -0
  30. package/dist/widgets/webwriter-task-prompt.js +68 -0
  31. package/dist/widgets/webwriter-task.css +444 -0
  32. package/dist/widgets/webwriter-task.js +546 -0
  33. package/dist/widgets/webwriter-text.css +444 -0
  34. package/dist/widgets/webwriter-text.js +174 -0
  35. package/package.json +199 -0
  36. package/src/lib/combobox.ts +456 -0
  37. package/src/lib/highlighter-fill.svg +6 -0
  38. package/src/snippets/choice.html +8 -0
  39. package/src/snippets/cloze.html +4 -0
  40. package/src/snippets/mark.html +4 -0
  41. package/src/snippets/order.html +8 -0
  42. package/src/snippets/pairing.html +10 -0
  43. package/src/snippets/speech.html +4 -0
  44. package/src/snippets/text.html +4 -0
  45. package/src/snippets/wordsearch.html +4 -0
  46. package/src/widgets/webwriter-choice-item.ts +251 -0
  47. package/src/widgets/webwriter-choice.ts +310 -0
  48. package/src/widgets/webwriter-cloze-gap.ts +217 -0
  49. package/src/widgets/webwriter-cloze.ts +136 -0
  50. package/src/widgets/webwriter-mark.ts +435 -0
  51. package/src/widgets/webwriter-order-item.ts +445 -0
  52. package/src/widgets/webwriter-order.ts +273 -0
  53. package/src/widgets/webwriter-pairing-item.ts +156 -0
  54. package/src/widgets/webwriter-pairing.ts +188 -0
  55. package/src/widgets/webwriter-quiz.ts +281 -0
  56. package/src/widgets/webwriter-speech.ts +268 -0
  57. package/src/widgets/webwriter-task-explainer.ts +38 -0
  58. package/src/widgets/webwriter-task-hint.ts +17 -0
  59. package/src/widgets/webwriter-task-prompt.ts +18 -0
  60. package/src/widgets/webwriter-task.ts +544 -0
  61. package/src/widgets/webwriter-text.ts +131 -0
  62. package/tsconfig.json +7 -0
@@ -0,0 +1,156 @@
1
+ import {html, css} from "lit"
2
+ import {LitElementWw, option} from "@webwriter/lit"
3
+ import {customElement, property} from "lit/decorators.js"
4
+ import "@shoelace-style/shoelace/dist/themes/light.css"
5
+
6
+ declare global {interface HTMLElementTagNameMap {
7
+ "webwriter-pairing-item": WebwriterPairingItem;
8
+ }}
9
+
10
+ @customElement("webwriter-pairing-item")
11
+ export class WebwriterPairingItem extends LitElementWw {
12
+
13
+ tabIndex = -1
14
+
15
+ static styles = css`
16
+
17
+ :host {
18
+ aspect-ratio: 1;
19
+ min-width: 125px;
20
+ max-width: 350px;
21
+ min-height: 125px;
22
+ max-height: 350px;
23
+ position: relative;
24
+ overflow: hidden;
25
+ overflow-y: auto;
26
+ scrollbar-width: thin;
27
+ resize: both;
28
+ border: 2px solid var(--sl-color-gray-500);
29
+ border-radius: 5px;
30
+ background: white;
31
+ }
32
+
33
+ slot {
34
+ width: 100%;
35
+ height: 100%;
36
+ display: flex;
37
+ box-sizing: border-box;
38
+ position: relative;
39
+ font-size: 0.8em;
40
+ z-index: 1;
41
+ }
42
+
43
+ :host([droppreview]) slot {
44
+ background: lightblue;
45
+ }
46
+
47
+ ::slotted(*) {
48
+ height: 100%;
49
+ width: 100%;
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ ::slotted(:not(:is(picture, audio, video, img, iframe))) {
54
+ padding: 5px !important;
55
+ }
56
+
57
+ #handle {
58
+ box-sizing: border-box;
59
+ border-radius: 2px;
60
+ position: absolute;
61
+ left: 1px;
62
+ bottom: 1px;
63
+ width: 20px;
64
+ height: 30px;
65
+ display: inline-block;
66
+ overflow: hidden;
67
+ line-height: 5px;
68
+ padding: 6px 4px;
69
+ cursor: move;
70
+ vertical-align: middle;
71
+ font-size: 12px;
72
+ font-family: sans-serif;
73
+ letter-spacing: 2px;
74
+ color: #cccccc;
75
+ text-shadow: 1px 0 1px black;
76
+ font-weight: 600;
77
+ opacity: 0.95;
78
+ z-index: 100;
79
+ }
80
+ #handle::after {
81
+ content: '.. .. ..';
82
+ }
83
+
84
+ :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
85
+ display: none !important;
86
+ }
87
+
88
+ #click-overlay {
89
+ position: absolute;
90
+ top: 0;
91
+ width: 100%;
92
+ height: 100%;
93
+ cursor: pointer;
94
+ }
95
+
96
+ #click-overlay:hover {
97
+ background: var(--sl-color-primary-600);
98
+ opacity: 10%;
99
+ }
100
+
101
+ :host(.ww-selected) #click-overlay {
102
+ display: none;
103
+ }
104
+ `
105
+
106
+ @property({attribute: true, reflect: true, converter: {toAttribute: (v: boolean) => v? "true": "false", fromAttribute: (attr: string) => attr === "true"}})
107
+ accessor draggable = true
108
+
109
+ @property({type: Boolean, attribute: true, reflect: true})
110
+ accessor dropPreview = false
111
+
112
+ connectedCallback(): void {
113
+ super.connectedCallback()
114
+ this.addEventListener("dragstart", e => {
115
+ if(e.target !== this) {
116
+ e.preventDefault()
117
+ }
118
+ // this.dropPreview = undefined
119
+ e.dataTransfer.setData("text/html", this.outerHTML)
120
+ e.dataTransfer.setData("text/plain", this.id)
121
+ // setTimeout(() => this.baseEl.style.visibility = "hidden")
122
+ e.stopPropagation()
123
+ })
124
+ this.addEventListener("drop", e => {
125
+ e.stopPropagation()
126
+ const id = e.dataTransfer.getData("text/plain")
127
+ const el = document.querySelector(`webwriter-pairing-item#${id}`)
128
+ if(!el) {
129
+ const html = e.dataTransfer.getData("text/html")
130
+ this.parentElement.insertAdjacentHTML("beforeend", html)
131
+ }
132
+ else if(el && !this.contains(el)) {
133
+ this.parentElement.insertAdjacentElement("beforeend", el)
134
+ }
135
+ this.dispatchEvent(new CustomEvent("ww-pairwith", {bubbles: true, detail: {with: id}}))
136
+ }, {passive: true})
137
+ this.addEventListener("dragover", e => {
138
+ this.dropPreview = true
139
+ e.preventDefault()
140
+ e.stopImmediatePropagation()
141
+ })
142
+ this.addEventListener("dragleave", () => this.dropPreview = false, {passive: true})
143
+ document.addEventListener("dragend", (e) => {
144
+ // (e.target as HTMLElement)?.remove()
145
+ this.dropPreview = false
146
+ }, {passive: true})
147
+ }
148
+
149
+ render() {
150
+ return html`
151
+ <slot></slot>
152
+ <div class="author-only" id="handle" @click=${() => this.dispatchEvent(new FocusEvent("focus"))}></div>
153
+
154
+ `
155
+ }
156
+ }
@@ -0,0 +1,188 @@
1
+ import {html, css, PropertyValues} from "lit"
2
+ import {LitElementWw, option} from "@webwriter/lit"
3
+ import {customElement, property, queryAll} from "lit/decorators.js"
4
+ import "@shoelace-style/shoelace/dist/themes/light.css"
5
+
6
+ import IconPlus from "bootstrap-icons/icons/plus.svg"
7
+ import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"
8
+ import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
9
+ import { WebwriterPairingItem } from "./webwriter-pairing-item"
10
+
11
+ declare global {interface HTMLElementTagNameMap {
12
+ "webwriter-pairing": WebwriterPairing;
13
+ }}
14
+
15
+ @customElement("webwriter-pairing")
16
+ export class WebwriterPairing extends LitElementWw {
17
+
18
+ static shadowRootOptions = {...LitElementWw.shadowRootOptions, slotAssignment: "manual" as const}
19
+
20
+ static scopedElements = {
21
+ "sl-button": SlButton,
22
+ "sl-icon": SlIcon
23
+ }
24
+
25
+ static styles = css`
26
+ :host {
27
+ display: flex !important;
28
+ flex-direction: row;
29
+ flex-wrap: wrap;
30
+ gap: 30px;
31
+ padding-bottom: 30px;
32
+ }
33
+
34
+ sl-button {
35
+
36
+ &::part(base) {
37
+ width: 125px;
38
+ height: 125px;
39
+ border: 1px solid darkgray;
40
+ border-radius: 2px;
41
+ padding: 10px;
42
+ overflow: hidden;
43
+ }
44
+
45
+ &::part(label) {
46
+ padding: 0;
47
+ display: flex;
48
+ flex-direction: row;
49
+ align-items: center;
50
+ justify-content: center;
51
+ }
52
+ }
53
+
54
+ .pair {
55
+ position: relative;
56
+
57
+ & .a-slot, & .b-slot {
58
+ display: block;
59
+
60
+ &:is(:focus-within, :has(::selection)) {
61
+ background: green;
62
+ }
63
+ }
64
+
65
+ & .b-slot {
66
+ position: absolute;
67
+ left: 20px;
68
+ top: 20px;
69
+ z-index: 1000;
70
+ }
71
+ }
72
+
73
+ `
74
+
75
+ @property({attribute: true, reflect: true})
76
+ @option({type: "select", options: [
77
+ {value: "drag"},
78
+ {value: "memory"}
79
+ ]})
80
+ accessor mode: "drag" | "memory" = "drag"
81
+
82
+ @property({type: Array, attribute: true, reflect: true})
83
+ accessor solution: ([string, string] | string)[] = []
84
+
85
+ addItem = () => {
86
+ const item = document.createElement("webwriter-pairing-item")
87
+ const picture = document.createElement("picture")
88
+ item.appendChild(picture)
89
+ this.appendChild(item)
90
+ }
91
+
92
+ Pair = (i: number) => {
93
+ return html`<div class="pair">
94
+ <slot class="a-slot" name=${`${i}a`}></slot>
95
+ <slot class="b-slot" name=${`${i}b`}></slot>
96
+ </div>`
97
+ }
98
+
99
+ getPairSlot() {}
100
+
101
+
102
+ get pairs() {
103
+ let allPairs = Array.from(this.children) as (WebwriterPairingItem | [WebwriterPairingItem, WebwriterPairingItem])[]
104
+ for(const [a, b] of this.solution) {
105
+ const elA = this.querySelector(`webwriter-pairing-item#${a}`) as WebwriterPairingItem
106
+ const elB = this.querySelector(`webwriter-pairing-item#${b}`) as WebwriterPairingItem
107
+ allPairs.splice(allPairs.indexOf(elA), 1, [elA, elB])
108
+ allPairs.splice(allPairs.indexOf(elB), 1)
109
+ }
110
+ return allPairs
111
+ }
112
+
113
+ pair(a: string, b?: string) {
114
+ let pairIds = this.pairs.map(entry => Array.isArray(entry)? [entry[0].id, entry[1].id]: entry.id)
115
+ const pairIndexOfA = this.solution.findIndex(val => Array.isArray(val) && val.includes(a))
116
+ const pairIndexOfB = this.solution.findIndex(val => Array.isArray(val) && val.includes(b))
117
+ if(!b) {
118
+ pairIds.splice(pairIndexOfA, 1, (pairIds[pairIndexOfA] as [string, string]).filter(id => id !== a)[0])
119
+ pairIds.push(a)
120
+ }
121
+ else {
122
+ if(pairIndexOfB !== -1) {
123
+ pairIds.splice(pairIndexOfB, 1, (pairIds[pairIndexOfB] as [string, string]).filter(id => id !== b)[0])
124
+ }
125
+ const indexOfA = this.solution.indexOf(a)
126
+ pairIds.splice(indexOfA, 1, [a, b])
127
+ pairIds = pairIds.filter(id => id !== b)
128
+ }
129
+ this.solution = pairIds.filter(entry => Array.isArray(entry)) as any
130
+ }
131
+
132
+ handlePairWith(e: CustomEvent) {
133
+ if(e.type === "ww-pairwith") {
134
+ this.pair((e.target as HTMLElement).id, e.detail.with)
135
+ }
136
+ }
137
+
138
+ observer: MutationObserver
139
+
140
+ connectedCallback(): void {
141
+ super.connectedCallback()
142
+ this.observer = new MutationObserver(() => {
143
+ let childIds = Array.from(this.children).map(child => child.id)
144
+ let solution = [...this.solution]
145
+ // solution = solution.filter(k => childIds.includes(k as any))
146
+ // solution = [...solution, ...childIds]
147
+ this.requestUpdate()
148
+ })
149
+ this.observer.observe(this, {childList: true})
150
+ this.addEventListener("ww-pairwith", this.handlePairWith)
151
+ }
152
+
153
+ disconnectedCallback(): void {
154
+ super.disconnectedCallback()
155
+ this.observer.disconnect()
156
+ }
157
+
158
+ protected willUpdate(_changedProperties: PropertyValues): void {
159
+ // this.solution = this.solution.filter(id => childIds.includes(id))
160
+ }
161
+
162
+ protected updated(_changedProperties: PropertyValues): void {
163
+ const slots = Array.from(this.slots)
164
+ for(const value of this.pairs) {
165
+ if(Array.isArray(value)) {
166
+ const [a, b] = value
167
+ slots.shift().assign(a)
168
+ slots.shift().assign(b)
169
+ }
170
+ else {
171
+ slots.shift().assign(value)
172
+ slots.shift()
173
+ }
174
+ }
175
+ }
176
+
177
+ @queryAll("slot")
178
+ accessor slots: NodeListOf<HTMLSlotElement>
179
+
180
+ render() {
181
+ return html`
182
+ ${this.pairs.map((el, i) => this.Pair(i))}
183
+ <sl-button @click=${this.addItem}>
184
+ <sl-icon src=${IconPlus}></sl-icon>
185
+ </sl-button>
186
+ `
187
+ }
188
+ }
@@ -0,0 +1,281 @@
1
+ import {css, html, PropertyValues} from "lit"
2
+ import {LitElementWw, option} from "@webwriter/lit"
3
+ import {customElement, property, query, queryAssignedElements} from "lit/decorators.js"
4
+
5
+ import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js"
6
+ import SlButtonGroup from "@shoelace-style/shoelace/dist/components/button-group/button-group.component.js"
7
+ import SlDropdown from "@shoelace-style/shoelace/dist/components/dropdown/dropdown.component.js"
8
+ import SlMenu from "@shoelace-style/shoelace/dist/components/menu/menu.component.js"
9
+ import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js"
10
+ import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js"
11
+ import IconUIChecksGrid from "bootstrap-icons/icons/ui-checks-grid.svg"
12
+ import Icon123 from "bootstrap-icons/icons/123.svg"
13
+ import IconCardText from "bootstrap-icons/icons/card-text.svg"
14
+ import IconHighlighter from "bootstrap-icons/icons/highlighter.svg"
15
+ import IconSubtract from "bootstrap-icons/icons/subtract.svg"
16
+ import IconBodyText from "bootstrap-icons/icons/body-text.svg"
17
+ import IconMic from "bootstrap-icons/icons/mic.svg"
18
+ import IconImage from "bootstrap-icons/icons/image.svg"
19
+ import IconSearch from "bootstrap-icons/icons/search.svg"
20
+ import IconGrid3x3Gap from "bootstrap-icons/icons/grid-3x3-gap.svg"
21
+
22
+ import "@shoelace-style/shoelace/dist/themes/light.css"
23
+ import { WebwriterTask } from "./webwriter-task.js"
24
+
25
+
26
+ function shuffle<T>(a: T[]) {
27
+ for (let i = a.length - 1; i > 0; i--) {
28
+ const j = Math.floor(Math.random() * (i + 1));
29
+ [a[i], a[j]] = [a[j], a[i]];
30
+ }
31
+ return a;
32
+ }
33
+
34
+ declare global {interface HTMLElementTagNameMap {
35
+ "webwriter-quiz": WebwriterQuiz;
36
+ }}
37
+
38
+ @customElement("webwriter-quiz")
39
+ export class WebwriterQuiz extends LitElementWw {
40
+
41
+ static localization = {}
42
+
43
+ msg = (str: string) => this.lang in WebwriterQuiz.localization? WebwriterQuiz.localization[this.lang][str] ?? str: str
44
+
45
+ get answerTypes() {
46
+ return {
47
+ "webwriter-choice": {
48
+ label: this.msg("Choice"),
49
+ icon: IconUIChecksGrid
50
+ },
51
+ "webwriter-order": {
52
+ label: this.msg("Order"),
53
+ icon: Icon123
54
+ },
55
+ "webwriter-text": {
56
+ label: this.msg("Text"),
57
+ icon: IconCardText
58
+ },
59
+ "webwriter-mark": {
60
+ label: this.msg("Mark"),
61
+ icon: IconHighlighter
62
+ },/*
63
+ "webwriter-pairing": {
64
+ label: this.msg("Pairing"),
65
+ icon: IconSubtract
66
+ },
67
+ "webwriter-cloze": {
68
+ label: this.msg("Cloze"),
69
+ icon: IconBodyText
70
+ },*/
71
+ "webwriter-speech": {
72
+ label: this.msg("Speech"),
73
+ //advanced: true,
74
+ icon: IconMic
75
+ },
76
+ /*"webwriter-wordsearch": {
77
+ label: this.msg("Word Search"),
78
+ advanced: true,
79
+ icon: IconSearch
80
+ },
81
+ "webwriter-memory": {
82
+ label: this.msg("Memory"),
83
+ advanced: true,
84
+ icon: IconGrid3x3Gap
85
+ }*/
86
+ }
87
+ }
88
+
89
+ static scopedElements = {
90
+ "sl-button": SlButton,
91
+ "sl-button-group": SlButtonGroup,
92
+ "sl-icon": SlIcon,
93
+ "sl-dropdown": SlDropdown,
94
+ "sl-menu": SlMenu,
95
+ "sl-menu-item": SlMenuItem,
96
+ }
97
+
98
+ addTask(answerTypeName: string) {
99
+ const task = this.ownerDocument.createElement("webwriter-task") as WebwriterTask
100
+ task.setAttribute("counter", this.counter)
101
+ const prompt = this.ownerDocument.createElement("webwriter-task-prompt")
102
+ const p = this.ownerDocument.createElement("p")
103
+ prompt.append(p)
104
+ prompt.slot = "prompt"
105
+ const answer = this.ownerDocument.createElement(answerTypeName)
106
+ task.appendChild(prompt)
107
+ task.appendChild(answer)
108
+ this.appendChild(task)
109
+ document.getSelection().setBaseAndExtent(p, 0, p, 0)
110
+ }
111
+
112
+ static styles = css`
113
+ :host {
114
+ border-top: 1px solid darkgray;
115
+ border-bottom: 1px solid darkgray;
116
+ padding: 1ch;
117
+ display: flex !important;
118
+ flex-direction: column;
119
+ gap: 2rem;
120
+ counter-reset: task;
121
+ }
122
+
123
+ :host(:not([contenteditable=true]):not([contenteditable=""])) .author-only {
124
+ display: none;
125
+ }
126
+
127
+ :host(:is([contenteditable=true], [contenteditable=""])) .user-only {
128
+ display: none;
129
+ }
130
+
131
+ :host(:is([contenteditable=true], [contenteditable=""])) ::slotted(*) {
132
+ order: unset !important;
133
+ }
134
+
135
+ sl-button-group::part(base) {
136
+ display: flex;
137
+ }
138
+
139
+ sl-button-group > *:not(sl-dropdown) {
140
+ flex-grow: 1;
141
+ }
142
+
143
+ sl-dropdown[data-empty] {
144
+ display: none;
145
+ }
146
+
147
+ sl-button:not([caret])::part(label) {
148
+ padding: 0;
149
+ display: flex;
150
+ flex-direction: row;
151
+ width: 100%;
152
+ justify-content: space-evenly;
153
+ align-items: center;
154
+ }
155
+
156
+ sl-icon {
157
+ width: 18px;
158
+ height: 18px;
159
+ }
160
+
161
+ sl-menu::part(menu) {
162
+ z-index: 10000;
163
+ }
164
+
165
+ .user-actions {
166
+ & #submit {
167
+ flex-grow: 3;
168
+ }
169
+
170
+ & #reset {
171
+ flex-grow: 1;
172
+ }
173
+ }
174
+ `
175
+
176
+ @property({type: Boolean, attribute: true, reflect: true})
177
+ @option({type: Boolean, label: {"en": "Random Task Order"}})
178
+ accessor randomOrder = false
179
+
180
+ get counter(): "number" | "roman" | "roman-capitalized" | "alphabetical" | "alphabetical-capitalized" {
181
+ return this.tasks[0]?.counter
182
+ }
183
+
184
+ @property({attribute: true}) //@ts-ignore
185
+ @option({
186
+ type: "select",
187
+ label: {
188
+ "en": "Counter"
189
+ },
190
+ options: [
191
+ {value: undefined, label: {"en": "None"}},
192
+ {value: "number", label: {"en": "1. 2. 3."}},
193
+ {value: "roman", label: {"en": "i. ii. iii."}},
194
+ {value: "roman-capitalized", label: {"en": "I. II. III."}},
195
+ {value: "alphabetical", label: {"en": "a. b. c."}},
196
+ {value: "alphabetical-capitalized", label: {"en": "A. B. C."}},
197
+ ]
198
+ })
199
+ set counter(value) {
200
+ this.tasks.forEach(el => el.counter = value)
201
+ }
202
+
203
+ shuffleTasks() {
204
+ const n = this.tasks.length
205
+ const nums = shuffle([...(new Array(n)).keys()])
206
+ this.tasks.forEach((el, i) => el.style.order = String(nums[i]))
207
+ }
208
+
209
+ handleSubmit(e: Event) {
210
+ this.submitted = true
211
+ this.dispatchEvent(new Event("submit"))
212
+ this.tasks.forEach(task => task.handleSubmit())
213
+ }
214
+
215
+ handleReset = () => {
216
+ this.tasks.forEach(task => task.reset())
217
+ this.requestUpdate()
218
+ this.submitted = false
219
+ }
220
+
221
+ observer: MutationObserver
222
+
223
+ connectedCallback(): void {
224
+ super.connectedCallback()
225
+ this.observer = new MutationObserver(() => {
226
+ if(!this.contentEditable && this.randomOrder) {
227
+ this.shuffleTasks()
228
+ }
229
+ })
230
+ this.observer.observe(this, {childList: true})
231
+ }
232
+
233
+ disconnectedCallback(): void {
234
+ super.disconnectedCallback()
235
+ this.observer.disconnect()
236
+ }
237
+
238
+ @query("slot")
239
+ accessor slotEl: HTMLSlotElement
240
+
241
+ @queryAssignedElements()
242
+ accessor tasks: WebwriterTask[]
243
+
244
+ @property({type: Boolean, attribute: true, reflect: true})
245
+ accessor submitted = false
246
+
247
+ get isChanged() {
248
+ return this.tasks.some(task => task.isChanged)
249
+ }
250
+
251
+ render() {
252
+ const basicAnswerTypes = Object.keys(this.answerTypes).filter(k => !this.answerTypes[k]?.advanced)
253
+ const otherAnswerTypes = Object.keys(this.answerTypes).filter(k => this.answerTypes[k]?.advanced)
254
+ return html`
255
+ <slot ?inert=${this.submitted} @ww-answer-change=${() => this.requestUpdate()}></slot>
256
+ <sl-button-group class="user-only user-actions">
257
+ <sl-button id="submit" @click=${this.handleSubmit}>Submit</sl-button>
258
+ <sl-button ?disabled=${!this.isChanged} id="reset" @click=${this.handleReset}>Reset</sl-button>
259
+ </sl-button-group>
260
+ <sl-button-group class="author-only">
261
+ ${basicAnswerTypes.map(k => html`
262
+ <sl-button @click=${() => this.addTask(k)}>
263
+ <sl-icon src=${this.answerTypes[k]?.icon}></sl-icon>
264
+ ${this.answerTypes[k].label}
265
+ </sl-button>
266
+ `)}
267
+ <sl-dropdown data-empty=${!otherAnswerTypes.length} placement="bottom-end" hoist>
268
+ <sl-button slot="trigger" caret></sl-button>
269
+ <sl-menu>
270
+ ${otherAnswerTypes.map(k => html`
271
+ <sl-menu-item @click=${() => this.addTask(k)}>
272
+ <sl-icon src=${this.answerTypes[k]?.icon}></sl-icon>
273
+ ${this.answerTypes[k].label}
274
+ </sl-menu-item>
275
+ `)}
276
+ </sl-menu>
277
+ </sl-dropdown>
278
+ </sl-button-group>
279
+ `
280
+ }
281
+ }