fcad-core-dragon 2.1.1 → 2.1.2

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 (160) hide show
  1. package/.editorconfig +7 -7
  2. package/.gitlab-ci.yml +124 -0
  3. package/.prettierrc +11 -11
  4. package/.vscode/extensions.json +8 -8
  5. package/.vscode/settings.json +46 -16
  6. package/CHANGELOG +520 -520
  7. package/README.md +57 -57
  8. package/documentation/.vitepress/config.js +114 -114
  9. package/documentation/api-examples.md +49 -49
  10. package/documentation/composants/app-base-button.md +58 -58
  11. package/documentation/composants/app-base-error-display.md +59 -59
  12. package/documentation/composants/app-base-popover.md +68 -68
  13. package/documentation/composants/app-comp-audio.md +75 -75
  14. package/documentation/composants/app-comp-branch-buttons.md +111 -111
  15. package/documentation/composants/app-comp-button-progress.md +53 -53
  16. package/documentation/composants/app-comp-carousel.md +53 -53
  17. package/documentation/composants/app-comp-container.md +53 -53
  18. package/documentation/composants/app-comp-input-checkbox-next.md +42 -42
  19. package/documentation/composants/app-comp-input-dropdown-next.md +34 -34
  20. package/documentation/composants/app-comp-input-radio-next.md +39 -39
  21. package/documentation/composants/app-comp-input-text-next.md +35 -35
  22. package/documentation/composants/app-comp-input-text-table-next.md +34 -34
  23. package/documentation/composants/app-comp-input-text-to-fill-dropdown-next.md +53 -53
  24. package/documentation/composants/app-comp-input-text-to-fill-next.md +31 -31
  25. package/documentation/composants/app-comp-jauge.md +31 -31
  26. package/documentation/composants/app-comp-menu-item.md +55 -55
  27. package/documentation/composants/app-comp-menu.md +29 -29
  28. package/documentation/composants/app-comp-navigation.md +41 -41
  29. package/documentation/composants/app-comp-note-call.md +53 -53
  30. package/documentation/composants/app-comp-note-credit.md +53 -53
  31. package/documentation/composants/app-comp-play-bar-next.md +53 -53
  32. package/documentation/composants/app-comp-pop-up-next.md +93 -93
  33. package/documentation/composants/app-comp-quiz-next.md +235 -235
  34. package/documentation/composants/app-comp-quiz-recall.md +53 -53
  35. package/documentation/composants/app-comp-svg-next.md +53 -53
  36. package/documentation/composants/app-comp-table-of-content.md +50 -50
  37. package/documentation/composants/app-comp-video-player.md +82 -82
  38. package/documentation/composants.md +46 -46
  39. package/documentation/composants_critiques/ModelPageComposant.md +53 -53
  40. package/documentation/composants_critiques/app-base-module.md +43 -43
  41. package/documentation/composants_critiques/app-base-page.md +48 -48
  42. package/documentation/composants_critiques/app-base.md +311 -311
  43. package/documentation/composants_critiques/main.md +15 -15
  44. package/documentation/demarrage.md +50 -50
  45. package/documentation/deploiement.md +57 -57
  46. package/documentation/index.md +33 -33
  47. package/documentation/markdown-examples.md +85 -85
  48. package/documentation/public/vite.svg +14 -14
  49. package/documentation/public/vuejs.svg +1 -1
  50. package/documentation/public/vuetify.svg +5 -5
  51. package/eslint.config.js +60 -60
  52. package/junit-report.xml +182 -0
  53. package/package.json +66 -59
  54. package/playwright/index.html +12 -0
  55. package/playwright/index.js +21 -0
  56. package/playwright-ct.config.js +95 -0
  57. package/src/$locales/en.json +157 -157
  58. package/src/$locales/fr.json +120 -120
  59. package/src/assets/data/onboardingMessages.json +47 -47
  60. package/src/components/AppBase.vue +1171 -1169
  61. package/src/components/AppBaseButton.vue +90 -95
  62. package/src/components/AppBaseErrorDisplay.vue +438 -438
  63. package/src/components/AppBaseFlipCard.vue +84 -84
  64. package/src/components/AppBaseModule.vue +1639 -1634
  65. package/src/components/AppBasePage.vue +867 -866
  66. package/src/components/AppBasePopover.vue +41 -41
  67. package/src/components/AppBaseSkeleton.vue +66 -66
  68. package/src/components/AppCompAudio.vue +261 -256
  69. package/src/components/AppCompBranchButtons.vue +508 -508
  70. package/src/components/AppCompButtonProgress.vue +137 -132
  71. package/src/components/AppCompCarousel.vue +342 -336
  72. package/src/components/AppCompContainer.vue +29 -29
  73. package/src/components/AppCompInputCheckBoxNx.vue +325 -323
  74. package/src/components/AppCompInputDropdownNx.vue +302 -299
  75. package/src/components/AppCompInputRadioNx.vue +287 -284
  76. package/src/components/AppCompInputTextNx.vue +156 -153
  77. package/src/components/AppCompInputTextTableNx.vue +205 -202
  78. package/src/components/AppCompInputTextToFillDropdownNx.vue +343 -340
  79. package/src/components/AppCompInputTextToFillNx.vue +316 -313
  80. package/src/components/AppCompJauge.vue +81 -81
  81. package/src/components/AppCompMenu.vue +6 -1
  82. package/src/components/AppCompMenuItem.vue +246 -240
  83. package/src/components/AppCompNavigation.vue +977 -972
  84. package/src/components/AppCompNoteCall.vue +167 -161
  85. package/src/components/AppCompNoteCredit.vue +496 -491
  86. package/src/components/AppCompPlayBarNext.vue +2290 -2288
  87. package/src/components/AppCompPopUpNext.vue +508 -504
  88. package/src/components/AppCompQuizNext.vue +515 -510
  89. package/src/components/AppCompQuizRecall.vue +355 -350
  90. package/src/components/AppCompSVGNext.vue +346 -346
  91. package/src/components/AppCompSettingsMenu.vue +177 -172
  92. package/src/components/AppCompTableOfContent.vue +433 -427
  93. package/src/components/AppCompVideoPlayer.vue +377 -377
  94. package/src/components/AppCompViewDisplay.vue +6 -6
  95. package/src/components/BaseModule.vue +55 -55
  96. package/src/composables/useIdleDetector.js +56 -56
  97. package/src/composables/useQuiz.js +89 -89
  98. package/src/composables/useTimer.js +172 -172
  99. package/src/directives/nvdaFix.js +53 -53
  100. package/src/externalComps/ModuleView.vue +22 -22
  101. package/src/externalComps/SummaryView.vue +91 -91
  102. package/src/main.js +493 -476
  103. package/src/module/stores/appStore.js +960 -947
  104. package/src/module/xapi/ADL.js +520 -520
  105. package/src/module/xapi/Crypto/Hasher.js +241 -241
  106. package/src/module/xapi/Crypto/WordArray.js +278 -278
  107. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  108. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  109. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  110. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  111. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  112. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  113. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  114. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  115. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  116. package/src/module/xapi/Crypto/index.js +53 -53
  117. package/src/module/xapi/Statement/activity.js +47 -47
  118. package/src/module/xapi/Statement/agent.js +55 -55
  119. package/src/module/xapi/Statement/group.js +26 -26
  120. package/src/module/xapi/Statement/index.js +259 -259
  121. package/src/module/xapi/Statement/statement.js +253 -253
  122. package/src/module/xapi/Statement/statementRef.js +23 -23
  123. package/src/module/xapi/Statement/substatement.js +22 -22
  124. package/src/module/xapi/Statement/verb.js +36 -36
  125. package/src/module/xapi/activitytypes.js +17 -17
  126. package/src/module/xapi/launch.js +157 -157
  127. package/src/module/xapi/utils.js +167 -167
  128. package/src/module/xapi/verbs.js +294 -294
  129. package/src/module/xapi/wrapper.js +1895 -1895
  130. package/src/module/xapi/xapiStatement.js +444 -444
  131. package/src/plugins/analytics.js +34 -34
  132. package/src/plugins/bus.js +12 -8
  133. package/src/plugins/gsap.js +17 -17
  134. package/src/plugins/helper.js +355 -358
  135. package/src/plugins/i18n.js +27 -26
  136. package/src/plugins/idb.js +227 -227
  137. package/src/plugins/save.js +37 -37
  138. package/src/plugins/scorm.js +287 -287
  139. package/src/plugins/xapi.js +11 -11
  140. package/src/public/index.html +33 -33
  141. package/src/router/index.js +57 -57
  142. package/src/router/routes.js +312 -312
  143. package/src/shared/generalfuncs.js +344 -344
  144. package/src/shared/validators.js +1018 -1018
  145. package/tests/component/AppBaseButton.spec.js +53 -0
  146. package/tests/component/pinia.spec.js +24 -0
  147. package/{src/components/tests__ → tests/unit}/AppBaseButton.spec.js +53 -53
  148. package/tests/unit/AppCompInputCheckBoxNx.spec.js +59 -0
  149. package/tests/unit/AppCompInputDropdownNx.spec.js +51 -0
  150. package/tests/unit/AppCompInputRadioNx.spec.js +59 -0
  151. package/tests/unit/AppCompInputTextNx.spec.js +44 -0
  152. package/tests/unit/AppCompInputTextTableNx.spec.js +77 -0
  153. package/tests/unit/AppCompInputTextToFillDropdownNx.spec.js +60 -0
  154. package/tests/unit/AppCompInputTextToFillNx.spec.js +45 -0
  155. package/tests/unit/AppCompQuizNext.spec.js +114 -0
  156. package/tests/unit/AppCompVideoPlayer.spec.js +177 -0
  157. package/{src/components/tests__ → tests/unit}/useTimer.spec.js +91 -91
  158. package/vitest.config.js +28 -19
  159. package/vitest.setup.js +28 -0
  160. package/src/components/AppBaseButton.test.js +0 -21
@@ -1,510 +1,515 @@
1
- <!-- About this Component--
2
- * Component to render a Quiz to user: question with serie of inputs
3
- * Receives the a quiz data object defined by user
4
- * Base on Quiz type will render a specific input type for the quiz
5
- * Example of use: in an page you can add a quiz/exercice by calling:
6
- ** <app-comp-quiz-next :quiz-data="my_quiz_data_json"/>
7
- ** where "my_quiz_data_json" is a json object of the your quiz/exercices
8
-
9
- -->
10
- <template>
11
- <app-base-error-display
12
- v-if="errorList && errorList.length"
13
- :error-group="'component'"
14
- :error-title="'ERREUR: COMPOSANT DE QUIZ'"
15
- :errors-list="errorList"
16
- />
17
- <v-row v-else>
18
- <v-col
19
- v-if="quizData"
20
- cols="12"
21
- :class="`container_quiz_${quizData.type_question}`"
22
- >
23
- <!--Show skeleton while quiz data is not ready-->
24
- <template v-if="!isReady">
25
- <app-base-skeleton
26
- :skeleton-text="''"
27
- :skeleton-type="skeletonType(quizData.type_question)"
28
- />
29
- {{ quizData.type_question }}
30
- </template>
31
- <!--Show when quiz data is ready-->
32
- <template v-else>
33
- <div :class="`quiz_${quizData.type_question}`">
34
- <div
35
- :id="`${quizData.type_question}_${quizData.id}`"
36
- class="quiz-question"
37
- >
38
- <span class="sr-only">question :</span>
39
- <div v-html="quizEnnonce" />
40
- </div>
41
-
42
- <component
43
- :is="dynamicInputComponent"
44
- v-bind="quizElement"
45
- :ref="`Qz_${quizData.id}`"
46
- v-model="response"
47
- @enable-submit="enableValidateButton"
48
- />
49
-
50
- <div class="btn-ctrl-quiz">
51
- <app-base-button
52
- :id="`btn_quiz_${quizData.id}`"
53
- ref="quiz"
54
- class="btn-quiz btn-main"
55
- :is-disabled="!isEnabled"
56
- :title="txtBtnSubmit"
57
- @click="handleValidation"
58
- >
59
- {{ txtBtnSubmit }}
60
- </app-base-button>
61
- </div>
62
- </div>
63
- </template>
64
- </v-col>
65
- <v-col cols="12" class="ctn-retro" aria-live="polite">
66
- <transition name="fade" mode="in-out">
67
- <div
68
- v-if="showRetro"
69
- :class="`retro_inline_wrapper retro_inline_${retroType}`"
70
- :aria-label="retroAriaLabel"
71
- >
72
- <div class="retro-title-container">
73
- <span class="retro-title">{{ retroTitle }}</span>
74
- </div>
75
- <div class="retro-text-container" v-html="retroHtml"></div>
76
- </div>
77
- </transition>
78
- </v-col>
79
- </v-row>
80
- </template>
81
- <script>
82
- import { mapState } from 'pinia'
83
- import { useAppStore } from '../module/stores/appStore'
84
- import { defineAsyncComponent } from 'vue'
85
- import { computed } from 'vue'
86
- import { validateQuizData } from '../shared/validators'
87
- export default {
88
- name: 'AppCompQuiz',
89
- components: {
90
- AppCompInputTextBox: defineAsyncComponent(
91
- () => import('./AppCompInputTextNx.vue')
92
- ),
93
- AppCompInputCheckBox: defineAsyncComponent(
94
- () => import('./AppCompInputCheckBoxNx.vue')
95
- ),
96
- AppCompInputDropdown: defineAsyncComponent(
97
- () => import('./AppCompInputDropdownNx.vue')
98
- ),
99
- AppCompInputRadio: defineAsyncComponent(
100
- () => import('./AppCompInputRadioNx.vue')
101
- ),
102
- AppCompInputTextToFillDropdown: defineAsyncComponent(
103
- () => import('./AppCompInputTextToFillDropdownNx.vue')
104
- ),
105
- AppCompInputTextTable: defineAsyncComponent(
106
- () => import('./AppCompInputTextTableNx.vue')
107
- ),
108
- AppCompInputTextToFill: defineAsyncComponent(
109
- () => import('./AppCompInputTextToFillNx.vue')
110
- )
111
- },
112
- inject: ['userInteraction'],
113
- provide() {
114
- return {
115
- quizAssociation: computed(() => this.quizAssociation),
116
- quizDragDrop: computed(() => this.quizDragDrop),
117
- quizSubmit: computed(() => this.quizSubmit), //added to explod comp^
118
- initQuizSelected: computed(() => this.initQuizSelected), //value of the saved answers of the dropdown quiz
119
- initQuizTable: computed(() => this.initQuizTable) //value of the saved answers of the table quiz
120
- }
121
- },
122
- props: {
123
- /* MAIN PROP USED FOR 2 WAY BINDING OF DATA PARENT&CHILD */
124
- modelValue: [Array, null],
125
- consigne: {
126
- type: Boolean,
127
- default: true
128
- },
129
- shuffleAnswers: {
130
- type: Boolean,
131
- default: false
132
- },
133
- quizData: {
134
- type: Object,
135
- validator: function (value) {
136
- const { errorInConsole } = validateQuizData(value)
137
- if (errorInConsole.length)
138
- for (let err of errorInConsole)
139
- console.warn(
140
- ` %cAppCompQuiz>>>${err}`,
141
- 'background: orange; color: white; display: block; margin:5px;'
142
- )
143
-
144
- return errorInConsole.length == 0
145
- },
146
- default: () => {}
147
- }
148
- },
149
-
150
- data() {
151
- return {
152
- totalAttempts: 0, //Total attempts to answer the quiz
153
- quizCompleted: false,
154
- quizSubmit: false,
155
- retroType: '',
156
- retroTitle: '',
157
- retroHtml: '',
158
- showRetro: false,
159
- errorList: [],
160
- response: [],
161
- isEnabled: false,
162
- justInitialized: null,
163
- previousResponse: null,
164
- skeletonCount: 5, //Number of skeleton lines to show
165
- isReady: false
166
- }
167
- },
168
- computed: {
169
- ...mapState(useAppStore, [
170
- 'getUserInteraction',
171
- 'getCurrentPage',
172
- 'getUserDataStatus',
173
- 'getModuleInfo'
174
- ]),
175
- quizElement() {
176
- const propMapping = {
177
- choix_reponse: 'inputData',
178
- texte_base: 'textBase',
179
- max_essai: 'maxEssai'
180
- }
181
- this.quizData
182
-
183
- const quiz = {}
184
-
185
- for (const key in this.quizData) {
186
- const mappedKey = propMapping[key] || key
187
- quiz[mappedKey] = this.quizData[key]
188
- }
189
-
190
- return quiz
191
- },
192
- retroAriaLabel() {
193
- let label = ''
194
- switch (this.retroType) {
195
- case 'neutral':
196
- label = this.$t('quizState.neutralAnswer')
197
- break
198
- case 'positive':
199
- label = this.$t('quizState.goodAnswer')
200
- break
201
- case 'negative':
202
- label = this.$t('quizState.badAnswer')
203
- break
204
- }
205
- return label
206
- },
207
-
208
- /**
209
- * @description
210
- * @returns {string} the componant of the quiz input
211
- */
212
- dynamicInputComponent() {
213
- const { type_question } = this.quizData
214
-
215
- let inputComponent
216
- switch (true) {
217
- case type_question == 'choix_unique':
218
- inputComponent = 'AppCompInputRadio'
219
- break
220
- case type_question == 'dropdown':
221
- inputComponent = 'AppCompInputDropdown'
222
- break
223
- case type_question == 'choix_mult':
224
- inputComponent = 'AppCompInputCheckBox'
225
- break
226
- case type_question == 'reponse_ouverte':
227
- inputComponent = 'AppCompInputTextBox'
228
- break
229
- case type_question == 'texte_troue':
230
- inputComponent = 'AppCompInputTextToFill'
231
- break
232
- case type_question == 'texte_tableau':
233
- inputComponent = 'AppCompInputTextTable'
234
- break
235
- case type_question == 'texte_troue_select':
236
- inputComponent = 'AppCompInputTextToFillDropdown'
237
- break
238
- default:
239
- inputComponent = `div`
240
- }
241
- return inputComponent
242
- },
243
-
244
- /**
245
- * @description controls the showin of warning in the page when a prop is wrongly passed
246
- */
247
- errorQuizItems() {
248
- const errors = validateQuizData(this.quizData)
249
- return errors
250
- },
251
-
252
- /**
253
- * @description track the initialized state of this component.
254
- * Indicate wheither the component is just starting.
255
- * This computed property is use to control when some event /action should happen
256
- */
257
- compJustInitialized() {
258
- return this.justInitialized
259
- },
260
-
261
- /**
262
- * @description Text to display for the quiz button
263
- * @return {String} string to use by the local
264
- * */
265
- txtBtnSubmit() {
266
- let str = ''
267
- const { solution } = this.quizData
268
-
269
- if (solution === null) str = this.$t('button.save')
270
- //There is more than on element in the solution
271
- else str = this.$t('button.quiz_verify')
272
-
273
- return str
274
- },
275
-
276
- /**
277
- * @description Ennonce of the quiz - parse to add image if defined
278
- */
279
-
280
- quizEnnonce() {
281
- const { ennonce } = this.quizData
282
- let _ennonce = ennonce
283
- return _ennonce
284
- }
285
- },
286
- watch: {
287
- getUserInteraction: {
288
- async handler() {
289
- if (!this.getPreviousAnswers(this.userInteraction.quizAnswers)) {
290
- return setTimeout(() => (this.isReady = true), 500)
291
- }
292
-
293
- this.previousResponse = await this.getPreviousAnswers(
294
- this.userInteraction.quizAnswers
295
- )
296
- this.response = this.previousResponse
297
- },
298
- immediate: true,
299
- deep: true
300
- }
301
- },
302
- created() {
303
- this.$bus.$on('input-error', this.showErrorsMessages)
304
- this.$bus.$on('hide-retro', this.hideShowRetro)
305
- this.justInitialized = true
306
- },
307
- beforeUnmount() {
308
- this.$bus.$off('input-error', this.showErrorsMessages)
309
- this.$bus.$off('hide-retro', this.hideShowRetro)
310
- },
311
-
312
- mounted() {
313
- if (import.meta.env.DEV) {
314
- this.showErrorsMessages(this.errorQuizItems)
315
- }
316
- setTimeout(() => {
317
- this.justInitialized = false
318
- }, 500)
319
- },
320
- methods: {
321
- showErrorsMessages(data) {
322
- if (!data || !data.errors) return
323
- if (data.e !== this.quizData.id) return
324
-
325
- const { errorList, errorInConsole } = data.errors
326
- this.errorList = errorList
327
-
328
- if (errorInConsole.length)
329
- errorInConsole.forEach((err) => {
330
- console.warn(
331
- ` %cAppCompQuiz>>>${err}`,
332
- 'background: orange; color: white; display: block; margin:5px;'
333
- )
334
- })
335
- },
336
-
337
- /**
338
- * @description disables the quiz
339
- * @todo must be expanded for what happens when the quiz is disabled
340
- */
341
- setQuizCompleted() {
342
- this.quizCompleted = true
343
- },
344
-
345
- /**
346
- * @description saves the submitted answer in the store
347
- * - Create new entry in userData for the quiz if it doesn't exist or update its data
348
- * - Send xapi competion statement of the quiz to LRS
349
- * @param {Object} result - contain the user answer and success of is reponse
350
- * deprecated param {Boolean} isCounted inticates in wich array it will ba saved, true = quizAnswers, false = pollAnswers
351
- */
352
- saveAnswer(result) {
353
- const { userAnswer, correctAnswer } = result
354
- const quizID = this.quizData.id
355
-
356
- if (
357
- !this.userInteraction.quizAnswers ||
358
- !this.userInteraction.quizAnswers[quizID]
359
- ) {
360
- this.userInteraction.quizAnswers = {
361
- ...this.userInteraction.quizAnswers,
362
- [quizID]: {
363
- value: userAnswer,
364
- total_attempts: this.totalAttempts
365
- }
366
- }
367
- } else {
368
- this.userInteraction.quizAnswers[quizID] = {
369
- value: userAnswer,
370
- total_attempts: this.totalAttempts
371
- }
372
- }
373
-
374
- const id = this.getCurrentPage.activityRef
375
- let aName = ''
376
- let text = ''
377
- let txtEnnonce = document.querySelector('.quiz-question div').innerText
378
-
379
- switch (true) {
380
- case id == 'A00':
381
- aName = 'Introduction'
382
- break
383
- case id == 'A99':
384
- aName = 'Conclusion'
385
- break
386
-
387
- default: {
388
- let d = id.replace('A', '').trim()
389
- d = parseInt(d)
390
- aName = `Exercice ${this.quizData.id} de ${this.$t(
391
- 'text.activity'
392
- )} ${d}`
393
- }
394
- }
395
-
396
- switch (this.$i18n.locale) {
397
- case 'fr':
398
- if (this.getModuleInfo.courseID)
399
- text = `${aName} de ${this.getModuleInfo.id} `
400
- else text = `Le ${this.getModuleInfo.id}`
401
- break
402
- case 'en':
403
- if (this.getModuleInfo.courseID)
404
- text = `${aName} of ${this.getModuleInfo.id}`
405
- else text = `The ${this.getModuleInfo.id}`
406
- break
407
- }
408
- //Creating custom statement
409
- const stmtQuiz = {
410
- id: `exercices/${this.quizData.id}`,
411
- verb: 'answered',
412
- definition: txtEnnonce,
413
- description: text,
414
- type: 'http://adlnet.gov/expapi/activities/cmi.interaction',
415
- result: {
416
- response: JSON.stringify({ ...userAnswer, correctAnswer })
417
- }
418
- }
419
-
420
- this.$bus.$emit('send-xapi-statement', stmtQuiz)
421
- },
422
-
423
- /**
424
- * @description gets the answers for this quiz from existing answers record
425
- * @param {Object} answers the list of quizes answered from the userInteraction
426
- */
427
- getPreviousAnswers(answers) {
428
- const { id, type_question } = this.quizData
429
-
430
- if (!answers || !answers[id]) return null
431
- const { value, total_attempts } = answers[id]
432
-
433
- this.totalAttempts = total_attempts
434
- if (this.quizLimitActive) this.quizCompleted = true //Signal that quiz has been completed
435
- let isExecption = ['choix_unique']
436
- if (isExecption.includes(type_question)) {
437
- this.isReady = true
438
- return (this.response = [value])
439
- }
440
-
441
- this.response = value.map((a) => (a = a.filled || a.selected))
442
- this.isReady = true
443
- return this.response
444
- },
445
- /**
446
- * @description method to initalize validation process and save the user answer
447
- * Call the validation method of the child component (input) and save tho store/LRS
448
- * Validation Will process will only proceed when the user responses has changed
449
- */
450
- handleValidation() {
451
- const id = this.quizData.id
452
- const child = this.$refs[`Qz_${id}`]
453
- const result = child.validateAnswer()
454
-
455
- this.hideShowRetro(child, true)
456
- this.retroType = result.retroType
457
-
458
- switch (true) {
459
- case result.retroType == 'retro_positive':
460
- this.retroTitle = this.quizData.retroaction.retro_positive.title
461
- this.retroHtml = this.quizData.retroaction.retro_positive.hypertext
462
- break
463
- case result.retroType == 'retro_negative':
464
- this.retroTitle = this.quizData.retroaction.retro_negative.title
465
- this.retroHtml = this.quizData.retroaction.retro_negative.hypertext
466
- break
467
- case result.retroType == 'retro_neutre':
468
- this.retroTitle = this.quizData.retroaction.retro_neutre.title
469
- this.retroHtml = this.quizData.retroaction.retro_neutre.hypertext
470
- break
471
- }
472
- // return if there no changes in the user response
473
- if (
474
- this.previousResponse &&
475
- JSON.stringify(this.previousResponse) == JSON.stringify(this.response)
476
- )
477
- return
478
- this.totalAttempts += 1
479
- this.saveAnswer(result)
480
- },
481
- /**
482
- * @description Enables/disables valdate button
483
- */
484
- enableValidateButton(value) {
485
- this.isEnabled = value
486
- },
487
- /**
488
- * @description hide /show the retroaction
489
- */
490
- hideShowRetro(el, value) {
491
- if (!el || !el.id) return
492
- if (el.id !== this.quizData.id) return
493
-
494
- this.showRetro = value
495
- },
496
- skeletonType(type) {
497
- if (type == 'reponse_ouverte') {
498
- return `quiz-texte`
499
- } else {
500
- return `quiz-choix`
501
- }
502
- }
503
- }
504
- }
505
- </script>
506
- <style lang="scss">
507
- .custom-control {
508
- z-index: 0;
509
- }
510
- </style>
1
+ <!-- About this Component--
2
+ * Component to render a Quiz to user: question with serie of inputs
3
+ * Receives the a quiz data object defined by user
4
+ * Base on Quiz type will render a specific input type for the quiz
5
+ * Example of use: in an page you can add a quiz/exercice by calling:
6
+ ** <app-comp-quiz-next :quiz-data="my_quiz_data_json"/>
7
+ ** where "my_quiz_data_json" is a json object of the your quiz/exercices
8
+
9
+ -->
10
+ <template>
11
+ <app-base-error-display
12
+ v-if="errorList && errorList.length"
13
+ :error-group="'component'"
14
+ :error-title="'ERREUR: COMPOSANT DE QUIZ'"
15
+ :errors-list="errorList"
16
+ />
17
+ <v-row v-else>
18
+ <v-col
19
+ v-if="quizData"
20
+ cols="12"
21
+ :class="`container_quiz_${quizData.type_question}`"
22
+ >
23
+ <!--Show skeleton while quiz data is not ready-->
24
+ <template v-if="!isReady">
25
+ <app-base-skeleton
26
+ :skeleton-text="''"
27
+ :skeleton-type="skeletonType(quizData.type_question)"
28
+ />
29
+ {{ quizData.type_question }}
30
+ </template>
31
+ <!--Show when quiz data is ready-->
32
+ <template v-else>
33
+ <div :class="`quiz_${quizData.type_question}`">
34
+ <div
35
+ :id="`${quizData.type_question}_${quizData.id}`"
36
+ class="quiz-question"
37
+ >
38
+ <span class="sr-only">question :</span>
39
+ <div v-html="quizEnnonce" />
40
+ </div>
41
+
42
+ <component
43
+ :is="dynamicInputComponent"
44
+ v-bind="quizElement"
45
+ :ref="`Qz_${quizData.id}`"
46
+ v-model="response"
47
+ @enable-submit="enableValidateButton"
48
+ />
49
+
50
+ <div class="btn-ctrl-quiz">
51
+ <app-base-button
52
+ :id="`btn_quiz_${quizData.id}`"
53
+ ref="quiz"
54
+ class="btn-quiz btn-main"
55
+ :is-disabled="!isEnabled"
56
+ :title="txtBtnSubmit"
57
+ @click="handleValidation"
58
+ >
59
+ {{ txtBtnSubmit }}
60
+ </app-base-button>
61
+ </div>
62
+ </div>
63
+ </template>
64
+ </v-col>
65
+ <v-col cols="12" class="ctn-retro" aria-live="polite">
66
+ <transition name="fade" mode="in-out">
67
+ <div
68
+ v-if="showRetro"
69
+ :class="`retro_inline_wrapper retro_inline_${retroType}`"
70
+ :aria-label="retroAriaLabel"
71
+ >
72
+ <div class="retro-title-container">
73
+ <span class="retro-title">{{ retroTitle }}</span>
74
+ </div>
75
+ <div class="retro-text-container" v-html="retroHtml"></div>
76
+ </div>
77
+ </transition>
78
+ </v-col>
79
+ </v-row>
80
+ </template>
81
+ <script>
82
+ import { mapState } from 'pinia'
83
+ import { useAppStore } from '../module/stores/appStore'
84
+ import { defineAsyncComponent } from 'vue'
85
+ import { computed } from 'vue'
86
+ import { validateQuizData } from '../shared/validators'
87
+ import { useI18n } from 'vue-i18n'
88
+
89
+ export default {
90
+ name: 'AppCompQuiz',
91
+ components: {
92
+ AppCompInputTextBox: defineAsyncComponent(
93
+ () => import('./AppCompInputTextNx.vue')
94
+ ),
95
+ AppCompInputCheckBox: defineAsyncComponent(
96
+ () => import('./AppCompInputCheckBoxNx.vue')
97
+ ),
98
+ AppCompInputDropdown: defineAsyncComponent(
99
+ () => import('./AppCompInputDropdownNx.vue')
100
+ ),
101
+ AppCompInputRadio: defineAsyncComponent(
102
+ () => import('./AppCompInputRadioNx.vue')
103
+ ),
104
+ AppCompInputTextToFillDropdown: defineAsyncComponent(
105
+ () => import('./AppCompInputTextToFillDropdownNx.vue')
106
+ ),
107
+ AppCompInputTextTable: defineAsyncComponent(
108
+ () => import('./AppCompInputTextTableNx.vue')
109
+ ),
110
+ AppCompInputTextToFill: defineAsyncComponent(
111
+ () => import('./AppCompInputTextToFillNx.vue')
112
+ )
113
+ },
114
+ inject: ['userInteraction'],
115
+ provide() {
116
+ return {
117
+ quizAssociation: computed(() => this.quizAssociation),
118
+ quizDragDrop: computed(() => this.quizDragDrop),
119
+ quizSubmit: computed(() => this.quizSubmit), //added to explod comp^
120
+ initQuizSelected: computed(() => this.initQuizSelected), //value of the saved answers of the dropdown quiz
121
+ initQuizTable: computed(() => this.initQuizTable) //value of the saved answers of the table quiz
122
+ }
123
+ },
124
+ props: {
125
+ /* MAIN PROP USED FOR 2 WAY BINDING OF DATA PARENT&CHILD */
126
+ modelValue: [Array, null],
127
+ consigne: {
128
+ type: Boolean,
129
+ default: true
130
+ },
131
+ shuffleAnswers: {
132
+ type: Boolean,
133
+ default: false
134
+ },
135
+ quizData: {
136
+ type: Object,
137
+ validator: function (value) {
138
+ const { errorInConsole } = validateQuizData(value)
139
+ if (errorInConsole.length)
140
+ for (let err of errorInConsole)
141
+ console.warn(
142
+ ` %cAppCompQuiz>>>${err}`,
143
+ 'background: orange; color: white; display: block; margin:5px;'
144
+ )
145
+
146
+ return errorInConsole.length == 0
147
+ },
148
+ default: () => {}
149
+ }
150
+ },
151
+ setup() {
152
+ const { t } = useI18n()
153
+ return { t }
154
+ },
155
+ data() {
156
+ return {
157
+ totalAttempts: 0, //Total attempts to answer the quiz
158
+ quizCompleted: false,
159
+ quizSubmit: false,
160
+ retroType: '',
161
+ retroTitle: '',
162
+ retroHtml: '',
163
+ showRetro: false,
164
+ errorList: [],
165
+ response: [],
166
+ isEnabled: false,
167
+ justInitialized: null,
168
+ previousResponse: null,
169
+ skeletonCount: 5, //Number of skeleton lines to show
170
+ isReady: false
171
+ }
172
+ },
173
+ computed: {
174
+ ...mapState(useAppStore, [
175
+ 'getUserInteraction',
176
+ 'getCurrentPage',
177
+ 'getUserDataStatus',
178
+ 'getModuleInfo'
179
+ ]),
180
+ quizElement() {
181
+ const propMapping = {
182
+ choix_reponse: 'inputData',
183
+ texte_base: 'textBase',
184
+ max_essai: 'maxEssai'
185
+ }
186
+ this.quizData
187
+
188
+ const quiz = {}
189
+
190
+ for (const key in this.quizData) {
191
+ const mappedKey = propMapping[key] || key
192
+ quiz[mappedKey] = this.quizData[key]
193
+ }
194
+
195
+ return quiz
196
+ },
197
+ retroAriaLabel() {
198
+ let label = ''
199
+ switch (this.retroType) {
200
+ case 'neutral':
201
+ label = this.$t('quizState.neutralAnswer')
202
+ break
203
+ case 'positive':
204
+ label = this.$t('quizState.goodAnswer')
205
+ break
206
+ case 'negative':
207
+ label = this.$t('quizState.badAnswer')
208
+ break
209
+ }
210
+ return label
211
+ },
212
+
213
+ /**
214
+ * @description
215
+ * @returns {string} the componant of the quiz input
216
+ */
217
+ dynamicInputComponent() {
218
+ const { type_question } = this.quizData
219
+
220
+ let inputComponent
221
+ switch (true) {
222
+ case type_question == 'choix_unique':
223
+ inputComponent = 'AppCompInputRadio'
224
+ break
225
+ case type_question == 'dropdown':
226
+ inputComponent = 'AppCompInputDropdown'
227
+ break
228
+ case type_question == 'choix_mult':
229
+ inputComponent = 'AppCompInputCheckBox'
230
+ break
231
+ case type_question == 'reponse_ouverte':
232
+ inputComponent = 'AppCompInputTextBox'
233
+ break
234
+ case type_question == 'texte_troue':
235
+ inputComponent = 'AppCompInputTextToFill'
236
+ break
237
+ case type_question == 'texte_tableau':
238
+ inputComponent = 'AppCompInputTextTable'
239
+ break
240
+ case type_question == 'texte_troue_select':
241
+ inputComponent = 'AppCompInputTextToFillDropdown'
242
+ break
243
+ default:
244
+ inputComponent = `div`
245
+ }
246
+ return inputComponent
247
+ },
248
+
249
+ /**
250
+ * @description controls the showin of warning in the page when a prop is wrongly passed
251
+ */
252
+ errorQuizItems() {
253
+ const errors = validateQuizData(this.quizData)
254
+ return errors
255
+ },
256
+
257
+ /**
258
+ * @description track the initialized state of this component.
259
+ * Indicate wheither the component is just starting.
260
+ * This computed property is use to control when some event /action should happen
261
+ */
262
+ compJustInitialized() {
263
+ return this.justInitialized
264
+ },
265
+
266
+ /**
267
+ * @description Text to display for the quiz button
268
+ * @return {String} string to use by the local
269
+ * */
270
+ txtBtnSubmit() {
271
+ let str = ''
272
+ const { solution } = this.quizData
273
+
274
+ if (solution === null) str = this.$t('button.save')
275
+ //There is more than on element in the solution
276
+ else str = this.$t('button.quiz_verify')
277
+
278
+ return str
279
+ },
280
+
281
+ /**
282
+ * @description Ennonce of the quiz - parse to add image if defined
283
+ */
284
+
285
+ quizEnnonce() {
286
+ const { ennonce } = this.quizData
287
+ let _ennonce = ennonce
288
+ return _ennonce
289
+ }
290
+ },
291
+ watch: {
292
+ getUserInteraction: {
293
+ async handler() {
294
+ if (!this.getPreviousAnswers(this.userInteraction.quizAnswers)) {
295
+ return setTimeout(() => (this.isReady = true), 500)
296
+ }
297
+
298
+ this.previousResponse = await this.getPreviousAnswers(
299
+ this.userInteraction.quizAnswers
300
+ )
301
+ this.response = this.previousResponse
302
+ },
303
+ immediate: true,
304
+ deep: true
305
+ }
306
+ },
307
+ created() {
308
+ this.$bus.$on('input-error', this.showErrorsMessages)
309
+ this.$bus.$on('hide-retro', this.hideShowRetro)
310
+ this.justInitialized = true
311
+ },
312
+ beforeUnmount() {
313
+ this.$bus.$off('input-error', this.showErrorsMessages)
314
+ this.$bus.$off('hide-retro', this.hideShowRetro)
315
+ },
316
+
317
+ mounted() {
318
+ if (import.meta.env.DEV) {
319
+ this.showErrorsMessages(this.errorQuizItems)
320
+ }
321
+ setTimeout(() => {
322
+ this.justInitialized = false
323
+ }, 500)
324
+ },
325
+ methods: {
326
+ showErrorsMessages(data) {
327
+ if (!data || !data.errors) return
328
+ if (data.e !== this.quizData.id) return
329
+
330
+ const { errorList, errorInConsole } = data.errors
331
+ this.errorList = errorList
332
+
333
+ if (errorInConsole.length)
334
+ errorInConsole.forEach((err) => {
335
+ console.warn(
336
+ ` %cAppCompQuiz>>>${err}`,
337
+ 'background: orange; color: white; display: block; margin:5px;'
338
+ )
339
+ })
340
+ },
341
+
342
+ /**
343
+ * @description disables the quiz
344
+ * @todo must be expanded for what happens when the quiz is disabled
345
+ */
346
+ setQuizCompleted() {
347
+ this.quizCompleted = true
348
+ },
349
+
350
+ /**
351
+ * @description saves the submitted answer in the store
352
+ * - Create new entry in userData for the quiz if it doesn't exist or update its data
353
+ * - Send xapi competion statement of the quiz to LRS
354
+ * @param {Object} result - contain the user answer and success of is reponse
355
+ * deprecated param {Boolean} isCounted inticates in wich array it will ba saved, true = quizAnswers, false = pollAnswers
356
+ */
357
+ saveAnswer(result) {
358
+ const { userAnswer, correctAnswer } = result
359
+ const quizID = this.quizData.id
360
+
361
+ if (
362
+ !this.userInteraction.quizAnswers ||
363
+ !this.userInteraction.quizAnswers[quizID]
364
+ ) {
365
+ this.userInteraction.quizAnswers = {
366
+ ...this.userInteraction.quizAnswers,
367
+ [quizID]: {
368
+ value: userAnswer,
369
+ total_attempts: this.totalAttempts
370
+ }
371
+ }
372
+ } else {
373
+ this.userInteraction.quizAnswers[quizID] = {
374
+ value: userAnswer,
375
+ total_attempts: this.totalAttempts
376
+ }
377
+ }
378
+
379
+ const id = this.getCurrentPage.activityRef
380
+ let aName = ''
381
+ let text = ''
382
+ let txtEnnonce = document.querySelector('.quiz-question div').innerText
383
+
384
+ switch (true) {
385
+ case id == 'A00':
386
+ aName = 'Introduction'
387
+ break
388
+ case id == 'A99':
389
+ aName = 'Conclusion'
390
+ break
391
+
392
+ default: {
393
+ let d = id.replace('A', '').trim()
394
+ d = parseInt(d)
395
+ aName = `Exercice ${this.quizData.id} de ${this.$t(
396
+ 'text.activity'
397
+ )} ${d}`
398
+ }
399
+ }
400
+
401
+ switch (this.$i18n.locale) {
402
+ case 'fr':
403
+ if (this.getModuleInfo.courseID)
404
+ text = `${aName} de ${this.getModuleInfo.id} `
405
+ else text = `Le ${this.getModuleInfo.id}`
406
+ break
407
+ case 'en':
408
+ if (this.getModuleInfo.courseID)
409
+ text = `${aName} of ${this.getModuleInfo.id}`
410
+ else text = `The ${this.getModuleInfo.id}`
411
+ break
412
+ }
413
+ //Creating custom statement
414
+ const stmtQuiz = {
415
+ id: `exercices/${this.quizData.id}`,
416
+ verb: 'answered',
417
+ definition: txtEnnonce,
418
+ description: text,
419
+ type: 'http://adlnet.gov/expapi/activities/cmi.interaction',
420
+ result: {
421
+ response: JSON.stringify({ ...userAnswer, correctAnswer })
422
+ }
423
+ }
424
+
425
+ this.$bus.$emit('send-xapi-statement', stmtQuiz)
426
+ },
427
+
428
+ /**
429
+ * @description gets the answers for this quiz from existing answers record
430
+ * @param {Object} answers the list of quizes answered from the userInteraction
431
+ */
432
+ getPreviousAnswers(answers) {
433
+ const { id, type_question } = this.quizData
434
+
435
+ if (!answers || !answers[id]) return null
436
+ const { value, total_attempts } = answers[id]
437
+
438
+ this.totalAttempts = total_attempts
439
+ if (this.quizLimitActive) this.quizCompleted = true //Signal that quiz has been completed
440
+ let isExecption = ['choix_unique']
441
+ if (isExecption.includes(type_question)) {
442
+ this.isReady = true
443
+ return (this.response = [value])
444
+ }
445
+
446
+ this.response = value.map((a) => (a = a.filled || a.selected))
447
+ this.isReady = true
448
+ return this.response
449
+ },
450
+ /**
451
+ * @description method to initalize validation process and save the user answer
452
+ * Call the validation method of the child component (input) and save tho store/LRS
453
+ * Validation Will process will only proceed when the user responses has changed
454
+ */
455
+ handleValidation() {
456
+ const id = this.quizData.id
457
+ const child = this.$refs[`Qz_${id}`]
458
+ const result = child.validateAnswer()
459
+
460
+ this.hideShowRetro(child, true)
461
+ this.retroType = result.retroType
462
+
463
+ switch (true) {
464
+ case result.retroType == 'retro_positive':
465
+ this.retroTitle = this.quizData.retroaction.retro_positive.title
466
+ this.retroHtml = this.quizData.retroaction.retro_positive.hypertext
467
+ break
468
+ case result.retroType == 'retro_negative':
469
+ this.retroTitle = this.quizData.retroaction.retro_negative.title
470
+ this.retroHtml = this.quizData.retroaction.retro_negative.hypertext
471
+ break
472
+ case result.retroType == 'retro_neutre':
473
+ this.retroTitle = this.quizData.retroaction.retro_neutre.title
474
+ this.retroHtml = this.quizData.retroaction.retro_neutre.hypertext
475
+ break
476
+ }
477
+ // return if there no changes in the user response
478
+ if (
479
+ this.previousResponse &&
480
+ JSON.stringify(this.previousResponse) == JSON.stringify(this.response)
481
+ )
482
+ return
483
+ this.totalAttempts += 1
484
+ this.saveAnswer(result)
485
+ },
486
+ /**
487
+ * @description Enables/disables valdate button
488
+ */
489
+ enableValidateButton(value) {
490
+ this.isEnabled = value
491
+ },
492
+ /**
493
+ * @description hide /show the retroaction
494
+ */
495
+ hideShowRetro(el, value) {
496
+ if (!el || !el.id) return
497
+ if (el.id !== this.quizData.id) return
498
+
499
+ this.showRetro = value
500
+ },
501
+ skeletonType(type) {
502
+ if (type == 'reponse_ouverte') {
503
+ return `quiz-texte`
504
+ } else {
505
+ return `quiz-choix`
506
+ }
507
+ }
508
+ }
509
+ }
510
+ </script>
511
+ <style lang="scss">
512
+ .custom-control {
513
+ z-index: 0;
514
+ }
515
+ </style>