@pocketprep/ui-kit 3.6.0 → 3.7.1

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.
@@ -0,0 +1,472 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref, watch } from 'vue'
3
+ import { dark as vDark } from '../../../directives'
4
+ import type { TChoiceKey } from './../question'
5
+ import { useQuestionContext } from './composables'
6
+ import { stripHtmlTags } from '../../../utils'
7
+ import Icon from '../../Icons/Icon.vue'
8
+ import MPMCRadioGroup from './MPMCRadioGroup.vue'
9
+
10
+ interface IMPMCRadioOptions {
11
+ choices: TChoiceKey[]
12
+ value: TChoiceKey | null
13
+ label?: string
14
+ }
15
+
16
+ const emit = defineEmits<{
17
+ 'emitSelectedMPMCChoice': [mpmcChoiceKeys: TChoiceKey[]]
18
+ }>()
19
+
20
+ const {
21
+ // questionEl is used by the breakpoint directive
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
+ questionEl,
24
+ question,
25
+ isMPMCQuestionCorrect,
26
+ reviewMode,
27
+ isDarkMode,
28
+ showMPMCAnswers,
29
+ selectedMPMCChoices,
30
+ } = useQuestionContext()
31
+
32
+ const mpmcRadioGrid = ref<IMPMCRadioOptions[] | undefined>(undefined)
33
+ const expandedPartNumbers = ref<number[]>([])
34
+
35
+ const mpmcLabels = computed(() => {
36
+ return question.value.mpmcLabels || []
37
+ })
38
+
39
+ const questionChoices = computed(() => {
40
+ const choices = question.value.choices
41
+
42
+ // Shuffle mpmc choices
43
+ return [ ...choices ].sort(() => Math.random() - 0.5)
44
+ })
45
+
46
+ const choiceKeysByLabelIndexObj = computed(() => {
47
+ // Create an object with the keys being the mpmc label indexes
48
+ // and the values being an array of corresponding choiceKeys to the mpmc label
49
+ const groupedChoiceKeyByLabelIndex: { [key: number]: TChoiceKey[] } = {}
50
+
51
+ questionChoices.value?.forEach(choice => {
52
+ const labelIndex = choice.labelIndex
53
+
54
+ if (labelIndex !== undefined) {
55
+ if (!groupedChoiceKeyByLabelIndex[labelIndex]) {
56
+ groupedChoiceKeyByLabelIndex[labelIndex] = []
57
+ }
58
+ groupedChoiceKeyByLabelIndex[labelIndex].push(choice.id as TChoiceKey)
59
+ }
60
+ })
61
+
62
+ return groupedChoiceKeyByLabelIndex
63
+ })
64
+
65
+ const choiceTextsByLabelIndex = computed(() => {
66
+ const grouped: { [key: number]: string[] } = {}
67
+
68
+ questionChoices.value?.forEach(choice => {
69
+ const labelIndex = choice.labelIndex
70
+
71
+ if (labelIndex !== undefined) {
72
+ if (!grouped[labelIndex]) {
73
+ grouped[labelIndex] = []
74
+ }
75
+ grouped[labelIndex].push(choice.text || '')
76
+ }
77
+ })
78
+
79
+ return grouped
80
+ })
81
+
82
+ const selectedChoiceTextsByLabelIndex = computed(() => {
83
+ const selectedTexts: { [key: number]: string } = {}
84
+
85
+ mpmcRadioGrid.value?.forEach((part, labelIndex) => {
86
+ if (part.value) {
87
+ const choiceIndex = choiceKeysByLabelIndexObj.value[labelIndex]?.findIndex(key => key === part.value)
88
+ if (choiceIndex !== undefined && choiceIndex !== -1) {
89
+ const choiceText = choiceTextsByLabelIndex.value[labelIndex]?.[choiceIndex]
90
+ if (choiceText) {
91
+ selectedTexts[labelIndex] = stripHtmlTags(choiceText)
92
+ }
93
+ }
94
+ }
95
+ })
96
+
97
+ return selectedTexts
98
+ })
99
+
100
+ const defaultRadioButtonGrid = computed(() => {
101
+ return mpmcLabels.value.map((_label, labelIndex) => {
102
+ const choices = choiceKeysByLabelIndexObj.value[labelIndex] || []
103
+ return {
104
+ choices: choices,
105
+ value: null as TChoiceKey | null,
106
+ } as IMPMCRadioOptions
107
+ })
108
+ })
109
+
110
+ onMounted(() => {
111
+ mpmcRadioGrid.value = convertSelectedMPMCChoiceToRadioBtnGrid()
112
+ })
113
+
114
+ const toggleChoiceDropdown = (labelIndex: number) => {
115
+ const includedPartNumberIndex = expandedPartNumbers.value.findIndex(part => part === labelIndex)
116
+ if (includedPartNumberIndex === -1) {
117
+ expandedPartNumbers.value.push(labelIndex)
118
+ } else {
119
+ expandedPartNumbers.value.splice(includedPartNumberIndex, 1)
120
+ }
121
+ }
122
+
123
+ const openChoiceDropdown = (labelIndex: number) => {
124
+ const includedPartNumberIndex = expandedPartNumbers.value.findIndex(part => part === labelIndex)
125
+ if (includedPartNumberIndex === -1) {
126
+ expandedPartNumbers.value.push(labelIndex)
127
+ } else {
128
+ expandedPartNumbers.value.splice(includedPartNumberIndex, 1)
129
+ }
130
+ }
131
+
132
+ const getRadioGroupChoiceKeySelection = (labelIndex: number) => {
133
+ return mpmcRadioGrid.value?.[labelIndex]?.value
134
+ }
135
+
136
+ const getRadioGroupChoiceKeys = (labelIndex: number) => {
137
+ const choices = mpmcRadioGrid.value?.[labelIndex]?.choices
138
+ return choices
139
+ }
140
+
141
+ const getRadioGroupChoicesText = (labelIndex: number) => {
142
+ return choiceTextsByLabelIndex.value[labelIndex] || []
143
+ }
144
+
145
+ const updateRadioPartSelection = (labelIndex: number, choiceKey: TChoiceKey | null) => {
146
+ const part = mpmcRadioGrid.value?.[labelIndex]
147
+ if (part) {
148
+ part.value = choiceKey
149
+ }
150
+ }
151
+
152
+ const correctPart = (labelIndex: number) => {
153
+ if (mpmcRadioGrid.value?.[labelIndex]) {
154
+ const partVal = mpmcRadioGrid.value[labelIndex]?.value
155
+ if (partVal) {
156
+ return partVal.startsWith('a')
157
+ }
158
+ return false
159
+ }
160
+ return false
161
+ }
162
+
163
+ const convertSelectedMPMCChoiceToRadioBtnGrid = () => {
164
+ const radioBtnGrid = defaultRadioButtonGrid.value
165
+
166
+ selectedMPMCChoices.value.forEach(choice => {
167
+ const choiceObj = questionChoices.value?.find(c => c.id === choice)
168
+ if (choiceObj?.labelIndex !== undefined) {
169
+ const labelIndex = choiceObj.labelIndex
170
+ const radioBtnGridPart = radioBtnGrid?.[labelIndex]
171
+ if (radioBtnGridPart) {
172
+ radioBtnGridPart.value = choice
173
+ }
174
+ }
175
+ })
176
+
177
+ return radioBtnGrid
178
+ }
179
+
180
+ const emitSelectedMPMCChoice = (mpmcChoiceKeys: TChoiceKey[]) => {
181
+ emit('emitSelectedMPMCChoice', mpmcChoiceKeys)
182
+ }
183
+
184
+ watch(mpmcRadioGrid, () => {
185
+ if (mpmcRadioGrid.value && (!reviewMode.value || !showMPMCAnswers.value)) {
186
+ const selectedRadioButtonChoices: TChoiceKey[] = []
187
+
188
+ mpmcRadioGrid.value.forEach((part) => {
189
+ if (part.value) {
190
+ selectedRadioButtonChoices.push(part.value)
191
+ }
192
+ })
193
+
194
+ emitSelectedMPMCChoice(selectedRadioButtonChoices)
195
+ }
196
+ }, { deep: true })
197
+
198
+ watch(showMPMCAnswers, () => {
199
+ if (showMPMCAnswers) {
200
+ expandedPartNumbers.value = []
201
+ }
202
+ })
203
+
204
+ watch(selectedMPMCChoices, () => {
205
+ if ((reviewMode.value || showMPMCAnswers.value)) {
206
+ const selectedRadioBtnGrid = convertSelectedMPMCChoiceToRadioBtnGrid()
207
+ mpmcRadioGrid.value = selectedRadioBtnGrid
208
+ }
209
+ })
210
+
211
+ </script>
212
+
213
+ <template>
214
+ <div
215
+ class="uikit-question-mpmc-choices-container"
216
+ >
217
+ <div
218
+ class="uikit-question-mpmc-choices-container__choices-container"
219
+ :class="{
220
+ 'uikit-question-mpmc-choices-container__choices-container--correct':
221
+ (showMPMCAnswers || reviewMode) && isMPMCQuestionCorrect,
222
+ 'uikit-question-mpmc-choices-container__choices-container--incorrect':
223
+ (showMPMCAnswers || reviewMode) && !isMPMCQuestionCorrect,
224
+ }"
225
+ v-dark="isDarkMode"
226
+ >
227
+ <div
228
+ v-for="(label, labelIndex) in mpmcLabels"
229
+ class="uikit-question-mpmc-choices-container__part-container"
230
+ :key="labelIndex"
231
+ v-dark="isDarkMode"
232
+ :tabindex="0"
233
+ role="button"
234
+ :aria-expanded="expandedPartNumbers.includes(labelIndex)"
235
+ :aria-label="
236
+ `${stripHtmlTags(label)}. ${expandedPartNumbers.includes(labelIndex) ? 'Expanded' : 'Collapsed'}.`
237
+ "
238
+ @keydown.enter="openChoiceDropdown(labelIndex)"
239
+ @keydown.space.prevent="openChoiceDropdown(labelIndex)"
240
+ >
241
+ <div
242
+ class="uikit-question-mpmc-choices-container__part"
243
+ :class="{
244
+ 'uikit-question-mpmc-choices-container__part--expanded':
245
+ expandedPartNumbers.includes(labelIndex),
246
+ }"
247
+ v-dark="isDarkMode"
248
+ @click.stop="toggleChoiceDropdown(labelIndex)"
249
+ >
250
+ <Icon
251
+ v-if="(showMPMCAnswers || reviewMode) && correctPart(labelIndex)"
252
+ v-dark="isDarkMode"
253
+ class="uikit-question-mpmc-choices-container__part-label-correct-icon"
254
+ type="check"
255
+ />
256
+ <Icon
257
+ v-if="(showMPMCAnswers || reviewMode) && !correctPart(labelIndex)"
258
+ v-dark="isDarkMode"
259
+ class="uikit-question-mpmc-choices-container__part-label-incorrect-icon"
260
+ type="incorrect"
261
+ />
262
+ <div
263
+ class="uikit-question-mpmc-choices-container__part-label"
264
+ :class="{
265
+ 'uikit-question-mpmc-choices-container__part-label--review-mode':
266
+ showMPMCAnswers || reviewMode,
267
+ }"
268
+ v-dark="isDarkMode"
269
+ >
270
+ {{ stripHtmlTags(label) }}
271
+ <Icon
272
+ class="uikit-question-mpmc-choices-container__toggle-part-icon"
273
+ :class="{
274
+ 'uikit-question-mpmc-choices-container__toggle-part-icon--up':
275
+ expandedPartNumbers.includes(labelIndex),
276
+ }"
277
+ v-dark="isDarkMode"
278
+ type="accordionArrow"
279
+ />
280
+ </div>
281
+ <div
282
+ v-if="selectedChoiceTextsByLabelIndex[labelIndex]"
283
+ class="uikit-question-mpmc-choices-container__selected-choice-text"
284
+ v-dark="isDarkMode"
285
+ >
286
+ {{ selectedChoiceTextsByLabelIndex[labelIndex] }}
287
+ </div>
288
+ </div>
289
+ <div
290
+ v-if="expandedPartNumbers.includes(labelIndex)"
291
+ >
292
+ <MPMCRadioGroup
293
+ :modelValue="getRadioGroupChoiceKeySelection(labelIndex)"
294
+ :show-answers="showMPMCAnswers"
295
+ class="uikit-question-mpmc-choices-container__radio-btns"
296
+ :choices="getRadioGroupChoiceKeys(labelIndex)"
297
+ :choices-text="getRadioGroupChoicesText(labelIndex)"
298
+ :is-dark-mode="isDarkMode"
299
+ :disabled="false"
300
+ @update:modelValue="updateRadioPartSelection(labelIndex, $event)"
301
+ />
302
+ </div>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </template>
307
+
308
+ <style lang="scss">
309
+ @use '@/styles/breakpoints' as *;
310
+ @use '@/styles/colors' as *;
311
+
312
+ .uikit-question-mpmc-choices-container {
313
+ width: 100%;
314
+ max-width: 492px;
315
+
316
+ &__choices-container {
317
+ cursor: pointer;
318
+ position: relative;
319
+ outline: none;
320
+ transition: 0.1s width ease;
321
+
322
+ &::after {
323
+ content: '';
324
+ position: absolute;
325
+ top: -8px;
326
+ bottom: -8px;
327
+ left: -8px;
328
+ right: -8px;
329
+ border-radius: 11px;
330
+ pointer-events: none;
331
+ }
332
+
333
+ &--incorrect {
334
+ &::after {
335
+ display: block;
336
+ border: 2px solid $pepper;
337
+ }
338
+
339
+ &--dark::after {
340
+ border-color: $rosa;
341
+ }
342
+ }
343
+
344
+ &--correct {
345
+ &::after {
346
+ display: block;
347
+ border: 2px solid $cadaverous;
348
+ }
349
+
350
+ &--dark::after {
351
+ border-color: $jungle-green;
352
+ }
353
+ }
354
+ }
355
+
356
+ &__part-container {
357
+ margin-bottom: 16px;
358
+ border-radius: 5px;
359
+ background: $white;
360
+ box-shadow: 0 1px 4px 0 rgba($ash, 0.30);
361
+ outline: none;
362
+ transition: box-shadow 0.2s ease;
363
+
364
+ &:focus {
365
+ border: 1px solid $brand-blue;
366
+ }
367
+
368
+
369
+ &--dark {
370
+ border-radius: 5px;
371
+ background: $brand-black;
372
+ box-shadow: 0 1px 4px 0 $jet;
373
+
374
+ &:focus {
375
+ border: 1px solid $banana-bread;
376
+ }
377
+ }
378
+ }
379
+
380
+ &__part {
381
+ cursor: pointer;
382
+ padding: 12px 36px 12px 15px;
383
+ align-items: center;
384
+ align-content: flex-start;
385
+ flex-wrap: wrap;
386
+ border-radius: 5px;
387
+ border: 0.5px solid rgba($pewter, 0.85);
388
+ background: $white;
389
+
390
+ &--dark {
391
+ background: $brand-black;
392
+ }
393
+
394
+ &--expanded {
395
+ border-radius: 5px 5px 0px 0px;
396
+ }
397
+ }
398
+
399
+ &__part-label-correct-icon,
400
+ &__part-label-incorrect-icon {
401
+ position: absolute;
402
+ right: 0;
403
+ margin-right: 12px;
404
+ }
405
+
406
+ &__part-label-correct-icon {
407
+ color: $cadaverous;
408
+ width: 18px;
409
+ height: 18px;
410
+
411
+ &--dark {
412
+ color: $jungle-green;
413
+ }
414
+ }
415
+
416
+ &__part-label-incorrect-icon {
417
+ color: $pepper;
418
+
419
+ &--dark {
420
+ color: $rosa;
421
+ }
422
+ }
423
+
424
+ &__part-label {
425
+ color: $brand-black;
426
+ font-size: 16px;
427
+ font-weight: 600;
428
+ line-height: 23px;
429
+ letter-spacing: -0.1px;
430
+
431
+ &--dark {
432
+ color: $barely-background;
433
+ }
434
+
435
+ &--review-mode {
436
+ width: 285px;
437
+ }
438
+ }
439
+
440
+ &__selected-choice-text {
441
+ color: $brand-black;
442
+ font-size: 14px;
443
+ font-weight: 400;
444
+ line-height: 19px;
445
+ margin-top: 4px;
446
+
447
+ &--dark {
448
+ color: $barely-background;
449
+ }
450
+ }
451
+
452
+ &__toggle-part-icon {
453
+ position: absolute;
454
+ margin-top: 8px;
455
+ margin-left: 8px;
456
+ color: $brand-blue;
457
+
458
+ &--up {
459
+ transform: rotate(180deg);
460
+ }
461
+
462
+ &--dark {
463
+ color: $banana-bread;
464
+ }
465
+ }
466
+
467
+ &__radio-btns {
468
+ width: 100%;
469
+ max-width: 492px;
470
+ }
471
+ }
472
+ </style>
@@ -0,0 +1,169 @@
1
+ <!-- eslint-disable max-len -->
2
+ <script setup lang="ts">
3
+ import RadioButton from '../../Forms/RadioButton.vue'
4
+ import { dark as vDark } from '../../../directives'
5
+ import type { TChoiceKey } from '../question'
6
+ import { stripHtmlTags } from '../../../utils'
7
+
8
+ interface Props {
9
+ isDarkMode?: boolean
10
+ choices?: TChoiceKey[]
11
+ choicesText?: string[]
12
+ showAnswers?: boolean
13
+ disabled?: boolean
14
+ }
15
+
16
+ const props = withDefaults(defineProps<Props>(), {
17
+ isDarkMode: false,
18
+ choices: () => [],
19
+ choicesText: () => [],
20
+ showAnswers: false,
21
+ disabled: false,
22
+ })
23
+
24
+ const selectedChoice = defineModel<TChoiceKey | null>({ default: null })
25
+
26
+ const selectChoice = (choiceKey: TChoiceKey) => {
27
+ selectedChoice.value = choiceKey
28
+ }
29
+
30
+ const radioButtonColor = (choice: TChoiceKey) => {
31
+ if (props.showAnswers) {
32
+ if (choice === selectedChoice.value && selectedChoice.value?.startsWith('a')) {
33
+ return 'green'
34
+ }
35
+
36
+ if (choice === selectedChoice.value && selectedChoice.value?.startsWith('d')) {
37
+ return 'gray'
38
+ }
39
+
40
+ if (choice.startsWith('a')) {
41
+ return 'green'
42
+ }
43
+ }
44
+
45
+ return 'blue'
46
+ }
47
+
48
+ </script>
49
+
50
+ <template>
51
+ <ul
52
+ v-if="choices && choices.length"
53
+ class="uikit-mpmc-radio-group"
54
+ :class="{
55
+ 'uikit-mpmc-radio-group--show-answer': showAnswers
56
+ }"
57
+ v-dark="isDarkMode"
58
+ role="radiogroup"
59
+ >
60
+ <li
61
+ v-for="(choice, index) in choices"
62
+ class="uikit-mpmc-radio-group__option"
63
+ :class="{
64
+ 'uikit-mpmc-radio-group__option--show-answer': showAnswers
65
+ }"
66
+ v-dark="isDarkMode"
67
+ role="radio"
68
+ :tabindex="disabled || showAnswers ? -1 : 0"
69
+ :aria-checked="choice === selectedChoice"
70
+ :aria-disabled="disabled || showAnswers"
71
+ :aria-label="`
72
+ ${stripHtmlTags(choicesText[index])}. ${choice === selectedChoice ? 'Selected' : 'Not selected'}.`"
73
+ :key="choice"
74
+ @click="!disabled && !showAnswers && selectChoice(choice)"
75
+ @keydown.enter.stop="!disabled && !showAnswers && selectChoice(choice)"
76
+ @keydown.space.prevent.stop="!disabled && !showAnswers && selectChoice(choice)"
77
+ >
78
+ <RadioButton
79
+ class="uikit-mpmc-radio-group__radio-btn"
80
+ :selected="(choice === selectedChoice) || (showAnswers && choice.startsWith('a'))"
81
+ :disabled="disabled"
82
+ :isDarkMode="isDarkMode"
83
+ :color="radioButtonColor(choice)"
84
+ @click="!disabled && !showAnswers && selectChoice(choice)"
85
+ />
86
+ <div
87
+ v-if="choicesText && choicesText.length > 0"
88
+ v-dark="props.isDarkMode"
89
+ class="uikit-mpmc-radio-group__choice-text"
90
+ :class="{
91
+ 'uikit-mpmc-radio-group__choice-text--distractor':
92
+ showAnswers && choice?.startsWith('d')
93
+ }"
94
+ @click="!disabled && !showAnswers && selectChoice(choice)"
95
+ >
96
+ {{ stripHtmlTags(choicesText[index]) }}
97
+ </div>
98
+ </li>
99
+ </ul>
100
+ </template>
101
+
102
+ <style lang="scss" scoped>
103
+ @use '@/styles/breakpoints' as *;
104
+ @use '@/styles/colors' as *;
105
+
106
+ .uikit-mpmc-radio-group {
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ display: block;
111
+ background: $white;
112
+ border-radius: 0px 0px 5px 5px;
113
+
114
+ &--dark {
115
+ background: $brand-black;
116
+ }
117
+
118
+ &--show-answer {
119
+ cursor: default;
120
+ }
121
+
122
+ &__option {
123
+ display: flex;
124
+ align-items: flex-start;
125
+ align-self: stretch;
126
+ padding: 11px 15px 12px 15px;
127
+ max-width: 492px;
128
+ border: 0.5px solid rgba($pewter, 0.85);
129
+ border-top: none;
130
+ outline: none;
131
+ transition: background-color 0.2s ease;
132
+
133
+ &:not(&--show-answer) {
134
+ &:focus {
135
+ border: 0.5px solid $brand-blue;
136
+ }
137
+ }
138
+
139
+ &--dark {
140
+ &:focus:not(.uikit-mpmc-radio-group__option--show-answer) {
141
+ border: 0.5px solid $banana-bread;
142
+ }
143
+ }
144
+ }
145
+
146
+ &__choice-text {
147
+ display: block;
148
+ margin-left: 12px;
149
+ color: $brand-black;
150
+ font-size: 16px;
151
+ font-weight: 500;
152
+ line-height: 23px;
153
+ letter-spacing: -0.1px;
154
+
155
+ &--dark {
156
+ color: $barely-background;
157
+ }
158
+
159
+ &--distractor {
160
+ text-decoration: line-through;
161
+ color: $ash;
162
+
163
+ &--dark {
164
+ color: $fog;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ </style>
@@ -162,6 +162,7 @@ import { useQuestionContext } from './composables'
162
162
  import type { IRadioOptions, TMatrixChoiceKey } from './../question'
163
163
  import BrandColors from '../../../pocketprep-export.module.scss'
164
164
  import { computed, onMounted, ref, watch } from 'vue'
165
+ import { stripHtmlTags } from '../../../utils'
165
166
 
166
167
  const emit = defineEmits<{
167
168
  'emitSelectedMatrixChoice': [matrixChoiceKeys: TMatrixChoiceKey[]]
@@ -186,15 +187,6 @@ const expandedRowNumbers = ref<number[]>([])
186
187
  const selectedColumnHeaders = ref<string[][]>([])
187
188
  const brandColors = BrandColors
188
189
 
189
- const stripHtmlTags = (string?: string) => {
190
- if (string) {
191
- const div = document.createElement('div')
192
- div.innerHTML = string
193
- return div.textContent || ''
194
- }
195
- return ''
196
- }
197
-
198
190
  const matrixChoiceLayout = computed(() => {
199
191
  return question.value.matrixChoiceLayout
200
192
  })
@@ -2,6 +2,7 @@
2
2
  import RadioButton from '../../Forms/RadioButton.vue'
3
3
  import { dark as vDark } from '../../../directives'
4
4
  import type { TMatrixChoiceKey } from '../question'
5
+ import { stripHtmlTags } from '../../../utils'
5
6
 
6
7
  interface Props {
7
8
  isDarkMode?: boolean
@@ -42,15 +43,6 @@ const radioButtonColor = (choice: TMatrixChoiceKey) => {
42
43
 
43
44
  return 'blue'
44
45
  }
45
-
46
- const stripHtmlTags = (string?: string) => {
47
- if (string) {
48
- const div = document.createElement('div')
49
- div.innerHTML = string
50
- return div.textContent || ''
51
- }
52
- return ''
53
- }
54
46
  </script>
55
47
 
56
48
  <template>