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