@webwriter/quiz 1.0.2 → 1.0.4

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