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,866 +1,867 @@
1
- <!--
2
- @ Description: a prototype to test refactoring of pageMixin paradigm and fonctionality
3
- @ What it does: The component received a data and validate the type of page that will be created
4
- The component update the store information for the type of page created and also the timeline
5
- @Note: Page not created with this component will not have the data automatically traked by the application
6
- -->
7
- <template>
8
- <div :id="pageData.id" :key="pageData.id" class="app-page" role="main">
9
- <div v-if="appDebugMode" class="debug-pageInfo" :class="{ open: push }">
10
- <button class="w-btn" @click="push = !push">&lt;</button>
11
- <div class="ctn">
12
- Activity info: Route : {{ $route.path }}
13
- <br />
14
-
15
- Activity id : {{ getDebugModeInfo.id }}
16
- <br />
17
- Nombre de page : {{ getDebugModeInfo.size }}
18
- <br />
19
- Page complete :
20
- <ul>
21
- <li v-for="(value, key) in getDebugModeInfo.progression" :key="key">
22
- {{ value }}
23
- </li>
24
- </ul>
25
- </div>
26
- </div>
27
- <slot v-if="!errorPage.length" name="content">Page</slot>
28
- <app-base-error-display
29
- v-else
30
- :error-group="'component'"
31
- :error-title="'ERREUR: CRÉATION DE LA PAGE'"
32
- :errors-list="errorPage"
33
- :error-text="`Vous avez une/des erreur(s) dans la création de votre PAGE. Veuillez
34
- corriger les erreurs ci-dessous:`"
35
- />
36
- <span
37
- id="page_info"
38
- ref="page_info"
39
- class="sr-only"
40
- aria-hidden="true"
41
- ></span>
42
- <div id="hiddenAlertContainer" role="alert" class="sr-only"></div>
43
- </div>
44
- </template>
45
-
46
- <script>
47
- import { computed } from 'vue'
48
- import { useAppStore } from '../module/stores/appStore'
49
- import { mapState, mapActions } from 'pinia'
50
-
51
- export default {
52
- provide() {
53
- return { userInteraction: computed(() => this.userInteraction) }
54
- },
55
- props: {
56
- pageData: {
57
- type: Object,
58
- required: true,
59
-
60
- validator(value) {
61
- let isValid = true
62
- if (import.meta.env.DEV) {
63
- const requiredPageKeys = ['id', 'activityRef', 'type']
64
- let requiredTypeValues = [
65
- 'pg_normal',
66
- 'pg_menu',
67
- 'pg_animation',
68
- 'pg_media',
69
- 'pg_branch'
70
- ]
71
-
72
- requiredPageKeys.forEach((key) => {
73
- if (!Object.keys(value).includes(key)) {
74
- console.warn(
75
- `%c WARNING!>>> PAGE: PAGE: Missing ${key} for the page in $data `,
76
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
77
- )
78
- isValid = false
79
- } else if (
80
- key === 'type' &&
81
- !requiredTypeValues.includes(value[key])
82
- ) {
83
- let errString = `Invalid value assigment for type of the page in $data.`
84
-
85
- console.warn(
86
- `%c WARNING!>>> PAGE: ${errString}`,
87
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
88
- )
89
- isValid = false
90
- }
91
- })
92
- }
93
- return isValid
94
- }
95
- }
96
- },
97
- setup(props) {
98
- const store = useAppStore()
99
- const { activityRef, id: pageID, type: pageType } = props.pageData
100
-
101
- //Getting initial existing userIntaction from store
102
- const { userInteraction: previousInteraction = {} } =
103
- store.getPageInteraction(activityRef, pageID)
104
-
105
- return { store, previousInteraction, pageID, pageType }
106
- },
107
-
108
- data() {
109
- return {
110
- getHash: null,
111
- //========PageMisins values ===================
112
- userInteraction: this.previousInteraction,
113
- state: null,
114
- anchorEnable: true,
115
- anchorInfo: null,
116
- drModeActive: null,
117
- error: null,
118
- id: this.pageID,
119
- type: this.pageType,
120
- push: false
121
-
122
- //======== End PageMisins values ===================
123
- }
124
- },
125
- computed: {
126
- ...mapState(useAppStore, [
127
- 'getDataNoteCredit',
128
- 'getAllActivities',
129
- 'getApplicationSettings',
130
- 'getAppDebugMode'
131
- ]),
132
-
133
- appDebugMode() {
134
- return this.getAppDebugMode
135
- },
136
- getRouteHistory() {
137
- return this.store.getRouteHistory
138
- },
139
- getAllActivitiesState() {
140
- return this.store.getAllActivitiesState
141
- },
142
-
143
- getAllCompleted() {
144
- return this.store.getAllCompleted
145
- },
146
- getErrorChoiceDetect() {
147
- return this.store.getErrorChoiceDetect
148
- },
149
- getUserInteraction() {
150
- return this.store.getUserInteraction
151
- },
152
- getPageInteraction() {
153
- return this.store.getPageInteraction
154
- },
155
-
156
- getCurrentPage() {
157
- return this.store.getCurrentPage
158
- },
159
-
160
- getCurrentBranching() {
161
- return this.store.getCurrentBranching
162
- },
163
- updateCurrentBranching() {
164
- return this.store.updateCurrentBranching
165
- },
166
- getModuleInfo() {
167
- return this.store.getModuleInfo
168
- },
169
-
170
- getAllActivities() {
171
- return this.store.getAllActivities()
172
- },
173
- getConnectionInfo() {
174
- return this.store.getConnectionInfo
175
- },
176
-
177
- getAnchorsForActivity() {
178
- return this.store.getAnchorsForActivity()
179
- },
180
- getBifChoice() {
181
- return this.store.getBifChoice
182
- },
183
-
184
- isBranchingPage() {
185
- return this.$route.meta.type === 'branching' && this.type !== 'pg_branch'
186
- },
187
-
188
- //================================================
189
- settingsOptions() {
190
- return this.settingsOptionsELPlus
191
- },
192
-
193
- settingsSelected() {
194
- const setting = this.getApplicationSettings
195
- return setting
196
- },
197
-
198
- errorPage() {
199
- let err = false
200
- if (import.meta.env.DEV) {
201
- const requiredPageKeys = ['id', 'activityRef', 'type']
202
- const errorList = []
203
- let count = 0
204
- let requiredTypeValues = [
205
- 'pg_normal',
206
- 'pg_menu',
207
- 'pg_animation',
208
- 'pg_media',
209
- 'pg_branch'
210
- ]
211
-
212
- requiredPageKeys.forEach((key) => {
213
- // required key is missing in $data that was passed for the page
214
- if (!Object.keys(this.pageData).includes(key)) {
215
- errorList.push(`Missing page ${key} in $data`)
216
- }
217
- // Validator value for type
218
- else if (
219
- key === 'type' &&
220
- !requiredTypeValues.includes(this.pageData[key])
221
- ) {
222
- errorList.push(`Invalid value assigment for page type in $data`)
223
- } else if (count < 1) {
224
- let errString = null
225
- const requiredValues = ['video', 'audio']
226
- switch (this.type) {
227
- // validation for animation page type content
228
-
229
- case 'pg_animation':
230
- if (!this.pageData.animation)
231
- errString = `Missing >>information in $data<< for animation type page`
232
- if (this.pageData.animation && !this.pageData.animation.refName)
233
- errString = `Missing >>refName<< for your animation `
234
- if (errString) {
235
- errorList.push(errString)
236
- console.warn(
237
- `%c WARNING!>>> PAGE: ${errString}`,
238
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
239
- )
240
- }
241
- break
242
- // valdation for media page type content
243
- case 'pg_media':
244
- if (
245
- !this.pageData.mediaData ||
246
- Object.keys(this.pageData.mediaData).length < 1
247
- ) {
248
- errString = `Missing >>media information<< for media type page`
249
- errorList.push(errString)
250
- console.warn(
251
- `%c WARNING!>>> PAGE: ${errString}`,
252
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
253
- )
254
- } else {
255
- // validation for mediaData content. Must have mType && mSource
256
- const { mType, mSources, mSubtitle, mTranscript, mPoster } =
257
- this.pageData.mediaData
258
- if (!mType || !mSources) {
259
- errString = `Missing key(s) in mediaData declaration for media`
260
- errorList.push(errString)
261
- console.warn(
262
- `%c WARNING!>>> PAGE: ${errString}`,
263
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
264
- )
265
- }
266
- // Validation for mType content. Must be video/audio
267
- else if (!requiredValues.includes(mType)) {
268
- errString = `Invalid declariation for media type must be audio or video`
269
- errorList.push(errString)
270
- console.warn(
271
- `%c WARNING!>>> PAGE: ${errString}`,
272
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
273
- )
274
- }
275
- // Validation for mSource content. Must have at least 1 media source definition
276
- else if (mSources && mSources.length < 1) {
277
- errString = `Missing>>information for media source(s)<< for media type page`
278
- errorList.push(errString)
279
- console.warn(
280
- `%c WARNING!>>> PAGE: ${errString}`,
281
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
282
- )
283
- }
284
- //Start validating that the files exsist
285
- if (!errString) {
286
- // start validating that the file for subtitle exist in medias folder
287
- if (mSubtitle && mSubtitle.src) return
288
-
289
- //validate that mPoster file is present in media file when set
290
- if (mPoster) {
291
- if (mPoster.constructor === String) return
292
- else {
293
- let errStringInconsole =
294
- '\n 💥 Invalid type declaration for mPoster.\n 🚩 Must be of type {String}'
295
-
296
- errString = `l'Attribut 👉 mPoster 👈 pour le media doit être de type {String}`
297
-
298
- errorList.push(errString)
299
- console.warn(
300
- `%c WARNING!>>> PAGE: ${errStringInconsole}`,
301
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
302
- )
303
- }
304
- if (errString) break
305
- }
306
-
307
- //validate that transcript file is present in media file when set
308
- if (mTranscript) {
309
- if (mTranscript.constructor === String) return
310
- else {
311
- let errStringInconsole =
312
- '\n 💥 Invalid type declaration for mTtranscript.\n 🚩 Must be of type {String}'
313
-
314
- errString = `l'Attribut 👉 mTranscript 👈 pour le media doit être de type {String}`
315
-
316
- errorList.push(errString)
317
- console.warn(
318
- `%c WARNING!>>> PAGE: ${errStringInconsole}`,
319
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
320
- )
321
- }
322
- if (errString) break
323
- }
324
- }
325
- }
326
- break
327
- }
328
- count++
329
- }
330
- })
331
- if (errorList.length > 0) err = errorList
332
- }
333
- return err
334
- },
335
-
336
- pgNumber() {
337
- if (!this.pageData.id) return
338
- let n = parseInt(this.pageData.id.replace('P', ''))
339
- return n
340
- },
341
-
342
- A11yPageInfo() {
343
- if (!this.$route || !this.$route.path) return ''
344
-
345
- let A11YTxt = ''
346
- const path = this.$route.path
347
- let reg = /[/|-]/g
348
-
349
- A11YTxt = path.replaceAll(reg, ' ') // replace all '/' and '-' by space
350
-
351
- if (A11YTxt.includes('activite'))
352
- A11YTxt = A11YTxt.replace('activite', this.$t('text.activity'))
353
-
354
- return A11YTxt.toLowerCase()
355
- },
356
-
357
- getDebugModeInfo() {
358
- const allActivitiesState = JSON.parse(
359
- JSON.stringify(this.getAllActivitiesState)
360
- )
361
- let size = allActivitiesState[this.pageData.activityRef]
362
- ? allActivitiesState[this.pageData.activityRef].size
363
- : 0
364
-
365
- let Pprogress = allActivitiesState[this.pageData.activityRef].progressions
366
-
367
- let info = {
368
- id: this.pageData.activityRef,
369
- size: size,
370
- progression: Pprogress
371
- }
372
-
373
- return info
374
- }
375
- },
376
- watch: {
377
- userInteraction: {
378
- async handler(newValue) {
379
- /**
380
- * Observe changes in the number of poperties to dispatch updates in the userdata in the Store
381
- */
382
-
383
- if (newValue && Object.entries(this.userInteraction).length) {
384
- await this.store.updateUserMetaData({
385
- activityRef: this.pageData.activityRef,
386
- id: this.pageData.id,
387
- userInteraction: { ...this.userInteraction }
388
- })
389
- }
390
- },
391
- immediate: true,
392
- deep: true
393
- },
394
- 'store.userDataLoaded': {
395
- async handler() {
396
- if (!this.store.userDataLoaded) return
397
- this.userInteraction = await this.setInitialInteraction()
398
- },
399
- immediate: true,
400
- deep: true
401
- }
402
- },
403
- created() {
404
- /*
405
- * Create a custome object for this page that will be added in the collection
406
- * will greate an id for the page
407
- * will create a data for the page. Data key can hold media info such as timeline of animation, url, media type
408
- * will update the store information for the currentPage
409
- * wil update the store information for the currentTimeline (GSap animation)
410
- */
411
-
412
- if (this.pageData && this.errorPage === false) {
413
- // Handeling presence of animation in the page
414
- if (this.pageData.animation) {
415
- // update the store with the information of currentTimeline
416
- }
417
-
418
- if (this.type == 'pg_branch') {
419
- // Update the store with the current page information when branching page is created
420
- // Note: Branching doesn't trigger router navigation so is done directly here
421
- this.store.updateCurrentPage({
422
- activity_Id: this.pageData.activityRef,
423
- page_Id: this.pageData.id
424
- })
425
- this.$bus.$on('branch-page-viewed', this.completePageBranching)
426
- }
427
- }
428
-
429
- if (this.isBranchingPage) this.updateCurrentBranching(this.$data)
430
- this.$bus.$on('media-viewed', this.setMediaViewed)
431
- this.$bus.$on('manage-media-players', this.managePlayingMedia)
432
- this.$bus.$on('video-transcript-toggle', this.onVideoTranscriptToggle)
433
- this.$bus.$on('save-quiz-answers', this.saveQuizAnswers)
434
- },
435
- mounted() {
436
- //Fix for firefox not updating aria-labelledby (was stuck saying Activite 1, page 1 on every pages)
437
- this.$refs['page_info'].innerHTML = this.A11yPageInfo
438
-
439
- // scrolling to top of only when is normal page
440
- if (this.pageData && this.type === 'pg_normal')
441
- this.$bus.$emit('move-to-target', 'page_info_section')
442
-
443
- // set the state of the page when page is mounted : started or completed
444
- if (this.userInteraction && this.userInteraction.state)
445
- this.state = this.userInteraction.state
446
- else if (
447
- this.type === 'pg_menu' &&
448
- this.userInteraction.state !== 'completed'
449
- ) {
450
- this.state = 'completed' // set menu page state to completed
451
- } else this.state = 'started' // set the default state to started
452
-
453
- this.userInteraction['state'] = this.state // add the state to the userInteraction
454
- // handle completion status of the page.
455
- if (
456
- document.documentElement.scrollHeight <=
457
- document.documentElement.clientHeight + 20 &&
458
- this.type !== 'pg_branch'
459
- ) {
460
- this.completePage()
461
- }
462
-
463
- if (this.type === 'pg_branch') {
464
- this.updateCurrentBranchPage(this.$data)
465
- }
466
-
467
- if (this.type == 'pg_branch') return //do not proceed to add listener when branch page
468
-
469
- window.addEventListener('scroll', this.onFirstScroll, { once: true }) //listener is removed when event fired
470
- },
471
- unmounted() {
472
- //cleaning up all the event listener
473
- this.$bus.$off('branch-page-viewed', this.completePageBranching)
474
- this.$bus.$off('media-viewed', this.setMediaViewed)
475
- this.$bus.$off('manage-media-players', this.managePlayingMedia)
476
- this.$bus.$off('video-transcript-toggle', this.onVideoTranscriptToggle)
477
- if (!this.isBranchingPage && this.type !== 'pg_branch')
478
- this.updateCurrentBranching(null) //unset current branching when leaving a branching page
479
- if (this.type == 'pg_branch') this.updateCurrentBranchPage(null) //unset current branch page when leaving a branch page
480
- this.$bus.$off('save-quiz-answers', this.saveQuizAnswers)
481
- window.removeEventListener('scroll', this.onFirstScroll, { once: true }) //in case user did not scroll
482
- window.removeEventListener('scroll', this.handleScroll)
483
- },
484
- methods: {
485
- ...mapActions(useAppStore, ['updateCurrentBranchPage']),
486
- //==============================================================
487
- onFirstScroll() {
488
- window.addEventListener('scroll', this.handleScroll)
489
- },
490
- /**
491
- * @description to handle the complete state for the gauge
492
- */
493
-
494
- handleScroll(event) {
495
- event
496
- /*
497
- * DocumentElement properties does not alway work properly on all Browser.
498
- * To Ensure reliable value of its properties on all.
499
- * Browser we will calculate the Document Height by taking the maximum of
500
- * body and documentElement height poperties.
501
- * ref:https://javascript.info/size-and-scroll-window
502
- */
503
-
504
- let scrollHeight = null
505
- let clientHeight = null
506
- let scrollTop = null
507
- scrollHeight = Math.max(
508
- document.body.scrollHeight,
509
- document.documentElement.scrollHeight,
510
- document.body.offsetHeight,
511
- document.documentElement.offsetHeight,
512
- document.body.clientHeight,
513
- document.documentElement.clientHeight
514
- )
515
- clientHeight = document.documentElement.clientHeight
516
- scrollTop = window.scrollY
517
-
518
- // //Set scroll limit reached at 150px above the document height.
519
- let scrollLimit = scrollHeight - 150
520
- let fullyScrolled = Math.round(clientHeight + scrollTop)
521
-
522
- //consider page completed when scolled value has reached or passed set limit
523
- if (fullyScrolled >= scrollLimit && this.state !== 'completed') {
524
- this.completePage()
525
- }
526
- },
527
- /**
528
- * @description set the state of the page to complete
529
- * @fires send-xapi-statement to AppBaseModule.vue
530
- */
531
- completePage() {
532
- if (
533
- ['pg_menu', 'pg_branch'].includes(this.type) ||
534
- this.state == 'completed' ||
535
- this.isBranchingPage
536
- )
537
- return
538
-
539
- this.state = 'completed'
540
- this.userInteraction.state = this.state
541
- },
542
-
543
- /**
544
- * @description set the state of the branching page to complete
545
- * @fires send-xapi-statement to AppBaseModule.vue
546
- */
547
- completePageBranching() {
548
- //Get the current branching from the store
549
- const currentBranching = this.getCurrentBranching
550
- if (!currentBranching || currentBranching.id !== this.$route.meta.id)
551
- return
552
- if (currentBranching.state === 'completed') return
553
-
554
- const children = this.$route.meta.children
555
- let count = 0
556
- children.forEach((c) => {
557
- let progress = this.getProgress(c._ref)
558
-
559
- if (progress.state == 'completed') count += 1
560
- })
561
-
562
- if (count !== children.length) return
563
-
564
- currentBranching.state = 'completed' //set the state of the page to completed
565
- currentBranching.userInteraction.state = currentBranching.state // update the useInteraction state
566
- },
567
- /**
568
- * @description Get the user progress for the current page
569
- * @param {string} id (Otpional) - the id of the targeted page
570
- * @return {Oject} - the existing user data for the current page
571
- */
572
- getProgress(id) {
573
- id = id || this.pageData.id
574
-
575
- const record = this.getPageInteraction(this.pageData.activityRef, id)
576
-
577
- if (Object.entries(record).length) {
578
- const { userInteraction } = record
579
- return userInteraction
580
- }
581
- return {}
582
- },
583
-
584
- anchorProgress() {
585
- const anchors = document.querySelectorAll('.anchor') // look for anchor
586
- const options = {
587
- root: null,
588
- threshold: 0
589
- }
590
-
591
- let anchorsComplete
592
- let indexStrt
593
- let anchorString
594
- let indexEnd
595
- let anchorComplete
596
-
597
- let target = document.querySelector('#App-base')
598
- // get anchor already seen
599
- anchorsComplete = this.getAnchorComplete()
600
-
601
- const observer = new IntersectionObserver((entries) => {
602
- // everytime the page passes a anchor
603
- observer.observe(target)
604
-
605
- entries.forEach((entry) => {
606
- // when it's visable in the page
607
-
608
- if (entry.isIntersecting) {
609
- // get the target
610
-
611
- this.anchorInfo = entry.target.classList
612
- indexStrt = this.anchorInfo.value.indexOf('anchor-')
613
- // work the string to get juste the anchor tag
614
- // must be the same as the class
615
- if (indexStrt == -1) {
616
- return
617
- }
618
-
619
- anchorString = this.anchorInfo.value.substring(indexStrt)
620
-
621
- indexEnd = anchorString.indexOf(' ')
622
- if (indexEnd != -1) {
623
- anchorComplete = anchorString.slice(0, indexEnd)
624
- } else {
625
- anchorComplete = anchorString
626
- }
627
-
628
- //get all the anchors of the current activity
629
- const anchors_list = this.getAnchorsForActivity(
630
- this.pageData.activityRef
631
- )
632
-
633
- //search for the current ancor
634
- const anc = anchors_list.find((a) => a.anchorTag === anchorComplete)
635
- //dispatch the current anchor to the store
636
- if (anc) {
637
- // update the store value for current section
638
- // this.updateCurrentSection(anc)
639
- // Ask bread scrumb to update its information
640
-
641
- this.$bus.$emit('anchor-seen', anchorComplete)
642
- } else {
643
- if (import.meta.env.DEV)
644
- console.warn(
645
- `%c WARNING!>>> Anchor handeling: 👉${anchorComplete}👈 doesn't exist. Please provide a valid anchor.`,
646
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
647
- )
648
-
649
- return
650
- }
651
-
652
- // if you didn't aldreay seen anchors
653
- if (anchorsComplete != undefined) {
654
- // look if you already saw this anchor
655
- if (anc && !anchorsComplete.includes(anchorComplete)) {
656
- // push it in the array them in the store
657
- anchorsComplete.push(anchorComplete)
658
- this.$set(this.userInteraction, 'anchors', anchorsComplete)
659
- }
660
- } else {
661
- // if you never saw any anchor
662
- // push it in the array them in the store
663
- anchorsComplete = []
664
- if (anc) anchorsComplete.push(anchorComplete)
665
- this.$set(this.userInteraction, 'anchors', anchorsComplete)
666
- }
667
- }
668
- })
669
- }, options)
670
-
671
- // observer call llok for each anchor in page
672
- anchors.forEach((anchor) => {
673
- observer.observe(anchor)
674
- })
675
- },
676
- getAnchorComplete() {
677
- const records = this.getPageInteraction(
678
- this.pageData.activityRef,
679
- this.pageData.id
680
- )
681
-
682
- // Verify if the element existe
683
- if (
684
- records &&
685
- records.userInteraction &&
686
- records.userInteraction.anchors
687
- ) {
688
- // if you already saw anchors return them
689
-
690
- return records.userInteraction.anchors
691
- }
692
- },
693
- showChoiceBif(data) {
694
- // check if you made a choice
695
- if (
696
- typeof this.getBifChoice === 'undefined' ||
697
- Object.keys(this.getBifChoice).length === 0
698
- ) {
699
- return data['A']
700
- } else {
701
- if (this.getBifChoice.choix) {
702
- // get the choise from store
703
- if (data.hasOwnProperty(this.getBifChoice.choix)) {
704
- let choice = data[this.getBifChoice.choix]
705
- //return choice
706
- return choice
707
- }
708
- }
709
- }
710
- },
711
- openPopup(data) {
712
- this.$bus.$emit('open-popup', data)
713
- },
714
- /**
715
- * @description Send to the bd that the media have been view by the user- Add the id of the media to the
716
- * userInteraction of the page
717
- */
718
- setMediaViewed(mediaID) {
719
- // Should get the userData to check if current mediaElement has entry
720
- let { mediasViewed } = this.userInteraction
721
-
722
- // Should create entry for medias viewed in userInteraction if none
723
- if (!mediasViewed) {
724
- mediasViewed = []
725
-
726
- this.userInteraction.mediasViewed = mediasViewed
727
- }
728
-
729
- //Should add ID in media viewed list if viewed for the 1st time
730
- if (mediasViewed.includes(mediaID)) return
731
-
732
- mediasViewed.push(mediaID)
733
-
734
- this.userInteraction.mediasViewed = mediasViewed //Update the userInteraction data of the page
735
- }, //
736
-
737
- /**
738
- * @description - Method to manage the playing state of media element in the page.
739
- * Only one media should play at a time. When receives signal of new media playing
740
- * get the reference of the previous media in play from the store and
741
- * put it in stop state. Also closes the transcript sidebar if it is open.
742
- * @param {HTMLElement} media - the actual media that is playing
743
- */
744
- managePlayingMedia(media) {
745
- if (!media) return
746
- //Should get all the media of the page from store
747
- const { mElements } = this.getCurrentPage
748
- if (!mElements || !mElements.length) return
749
-
750
- // // Should stop any media playing
751
- mElements.forEach((m) => {
752
- if (m.id == media.id) return
753
-
754
- const attrKeys = Object.keys(m) //
755
- const playbarInstance = m[attrKeys[3]]
756
- const HTMLmediaElement = m[attrKeys[1]]
757
-
758
- //Close the transcript sidebar if it is open
759
- if (playbarInstance.transcriptEnabled) {
760
- playbarInstance.toggleViewTranscript()
761
- }
762
- //Check if the media is playing to stop it. Playing state is given by the instance of the target play-bar
763
- if (playbarInstance.isPlaying) {
764
- HTMLmediaElement.pause() // target the HTMLmediaElement to control it state
765
- playbarInstance.isPlaying = false //change this isPlaying value of the instance
766
-
767
- playbarInstance.timer.pause() // pause the timer of the instance
768
- }
769
- })
770
- },
771
- /**
772
- * @description - Method to manage the state of the transcript and fullscreen buttons.
773
- * Disables those buttons on other media playbar when a transcript sidebar is open.
774
- *
775
- * @param {HTMLElement} media - the actual media that is playing
776
- * @param {Boolean} transcriptShown - current status of the transcript sidebar
777
- */
778
- onVideoTranscriptToggle(media, transcriptShown) {
779
- if (!media) return
780
- //Should get all the media of the page from store
781
- const { mElements } = this.getCurrentPage
782
- if (!mElements || !mElements.length) return
783
- mElements.forEach((m) => {
784
- if (m.id == media.id) return
785
- const attrKeys = Object.keys(m)
786
- const playbarInstance = m[attrKeys[3]]
787
- playbarInstance.otherVideoTranscriptShown = transcriptShown
788
- })
789
- },
790
- setInitialInteraction() {
791
- const { activityRef, id: pageID } = this.pageData
792
- const { userInteraction: previousInteraction = {} } =
793
- this.getPageInteraction(activityRef, pageID)
794
-
795
- return previousInteraction
796
- },
797
- saveQuizAnswers(el, quiz) {
798
- if (!this.userInteraction.quizAnswers)
799
- return (this.userInteraction.quizAnswers = { ...quiz })
800
- const quizID = Object.keys(quiz)[0]
801
-
802
- if (!this.userInteraction.quizAnswers[quizID])
803
- return (this.userInteraction.quizAnswers = {
804
- ...this.userInteraction.quizAnswers,
805
- ...quiz
806
- })
807
- const quizValue = Object.values(quiz)[0]
808
- this.userInteraction.quizAnswers[quizID] = quizValue
809
- // const {}
810
- }
811
- }
812
- }
813
- </script>
814
- <style lang="scss">
815
- .img-popUp {
816
- position: relative;
817
-
818
- .box-trigger {
819
- position: absolute;
820
- width: 100%;
821
- height: 100%;
822
- top: 0;
823
- left: 0;
824
-
825
- .btn {
826
- position: absolute;
827
- }
828
- }
829
- }
830
-
831
- .debug-pageInfo {
832
- position: fixed;
833
- right: -250px;
834
- bottom: 20px;
835
-
836
- color: #333;
837
- background-color: rgba(#eaabb6b3, 0.9);
838
-
839
- display: flex;
840
- flex-direction: row;
841
-
842
- &.open {
843
- right: 0;
844
- }
845
-
846
- .w-btn {
847
- padding: 10px;
848
- height: 100%;
849
- cursor: pointer;
850
- background: rgba(#fff, 0.05);
851
-
852
- &:hover {
853
- background: rgba(#fff, 0.1);
854
- }
855
- }
856
-
857
- .ctn {
858
- padding: 24px;
859
- width: 250px;
860
-
861
- ul {
862
- margin-left: 15px;
863
- }
864
- }
865
- }
866
- </style>
1
+ <!--
2
+ @ Description: a prototype to test refactoring of pageMixin paradigm and fonctionality
3
+ @ What it does: The component received a data and validate the type of page that will be created
4
+ The component update the store information for the type of page created and also the timeline
5
+ @Note: Page not created with this component will not have the data automatically traked by the application
6
+ -->
7
+ <template>
8
+ <div :id="pageData.id" :key="pageData.id" class="app-page" role="main">
9
+ <div v-if="appDebugMode" class="debug-pageInfo" :class="{ open: push }">
10
+ <button class="w-btn" @click="push = !push">&lt;</button>
11
+ <div class="ctn">
12
+ Activity info: Route : {{ $route.path }}
13
+ <br />
14
+
15
+ Activity id : {{ getDebugModeInfo.id }}
16
+ <br />
17
+ Nombre de page : {{ getDebugModeInfo.size }}
18
+ <br />
19
+ Page complete :
20
+ <ul>
21
+ <li v-for="(value, key) in getDebugModeInfo.progression" :key="key">
22
+ {{ value }}
23
+ </li>
24
+ </ul>
25
+ </div>
26
+ </div>
27
+ <slot v-if="!errorPage.length" name="content">Page</slot>
28
+ <app-base-error-display
29
+ v-else
30
+ :error-group="'component'"
31
+ :error-title="'ERREUR: CRÉATION DE LA PAGE'"
32
+ :errors-list="errorPage"
33
+ :error-text="`Vous avez une/des erreur(s) dans la création de votre PAGE. Veuillez
34
+ corriger les erreurs ci-dessous:`"
35
+ />
36
+ <span
37
+ id="page_info"
38
+ ref="page_info"
39
+ class="sr-only"
40
+ aria-hidden="true"
41
+ ></span>
42
+ <div id="hiddenAlertContainer" role="alert" class="sr-only"></div>
43
+ </div>
44
+ </template>
45
+
46
+ <script>
47
+ import { computed } from 'vue'
48
+ import { useAppStore } from '../module/stores/appStore'
49
+ import { mapState, mapActions } from 'pinia'
50
+ import { useI18n } from 'vue-i18n'
51
+
52
+ export default {
53
+ provide() {
54
+ return { userInteraction: computed(() => this.userInteraction) }
55
+ },
56
+ props: {
57
+ pageData: {
58
+ type: Object,
59
+ required: true,
60
+
61
+ validator(value) {
62
+ let isValid = true
63
+ if (import.meta.env.DEV) {
64
+ const requiredPageKeys = ['id', 'activityRef', 'type']
65
+ let requiredTypeValues = [
66
+ 'pg_normal',
67
+ 'pg_menu',
68
+ 'pg_animation',
69
+ 'pg_media',
70
+ 'pg_branch'
71
+ ]
72
+
73
+ requiredPageKeys.forEach((key) => {
74
+ if (!Object.keys(value).includes(key)) {
75
+ console.warn(
76
+ `%c WARNING!>>> PAGE: PAGE: Missing ${key} for the page in $data `,
77
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
78
+ )
79
+ isValid = false
80
+ } else if (
81
+ key === 'type' &&
82
+ !requiredTypeValues.includes(value[key])
83
+ ) {
84
+ let errString = `Invalid value assigment for type of the page in $data.`
85
+
86
+ console.warn(
87
+ `%c WARNING!>>> PAGE: ${errString}`,
88
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
89
+ )
90
+ isValid = false
91
+ }
92
+ })
93
+ }
94
+ return isValid
95
+ }
96
+ }
97
+ },
98
+ setup(props) {
99
+ const store = useAppStore()
100
+ const { activityRef, id: pageID, type: pageType } = props.pageData
101
+ const { t } = useI18n()
102
+ //Getting initial existing userIntaction from store
103
+ const { userInteraction: previousInteraction = {} } =
104
+ store.getPageInteraction(activityRef, pageID)
105
+
106
+ return { store, previousInteraction, pageID, pageType, t }
107
+ },
108
+
109
+ data() {
110
+ return {
111
+ getHash: null,
112
+ //========PageMisins values ===================
113
+ userInteraction: this.previousInteraction,
114
+ state: null,
115
+ anchorEnable: true,
116
+ anchorInfo: null,
117
+ drModeActive: null,
118
+ error: null,
119
+ id: this.pageID,
120
+ type: this.pageType,
121
+ push: false
122
+
123
+ //======== End PageMisins values ===================
124
+ }
125
+ },
126
+ computed: {
127
+ ...mapState(useAppStore, [
128
+ 'getDataNoteCredit',
129
+ 'getAllActivities',
130
+ 'getApplicationSettings',
131
+ 'getAppDebugMode'
132
+ ]),
133
+
134
+ appDebugMode() {
135
+ return this.getAppDebugMode
136
+ },
137
+ getRouteHistory() {
138
+ return this.store.getRouteHistory
139
+ },
140
+ getAllActivitiesState() {
141
+ return this.store.getAllActivitiesState
142
+ },
143
+
144
+ getAllCompleted() {
145
+ return this.store.getAllCompleted
146
+ },
147
+ getErrorChoiceDetect() {
148
+ return this.store.getErrorChoiceDetect
149
+ },
150
+ getUserInteraction() {
151
+ return this.store.getUserInteraction
152
+ },
153
+ getPageInteraction() {
154
+ return this.store.getPageInteraction
155
+ },
156
+
157
+ getCurrentPage() {
158
+ return this.store.getCurrentPage
159
+ },
160
+
161
+ getCurrentBranching() {
162
+ return this.store.getCurrentBranching
163
+ },
164
+ updateCurrentBranching() {
165
+ return this.store.updateCurrentBranching
166
+ },
167
+ getModuleInfo() {
168
+ return this.store.getModuleInfo
169
+ },
170
+
171
+ getAllActivities() {
172
+ return this.store.getAllActivities()
173
+ },
174
+ getConnectionInfo() {
175
+ return this.store.getConnectionInfo
176
+ },
177
+
178
+ getAnchorsForActivity() {
179
+ return this.store.getAnchorsForActivity()
180
+ },
181
+ getBifChoice() {
182
+ return this.store.getBifChoice
183
+ },
184
+
185
+ isBranchingPage() {
186
+ return this.$route.meta.type === 'branching' && this.type !== 'pg_branch'
187
+ },
188
+
189
+ //================================================
190
+ settingsOptions() {
191
+ return this.settingsOptionsELPlus
192
+ },
193
+
194
+ settingsSelected() {
195
+ const setting = this.getApplicationSettings
196
+ return setting
197
+ },
198
+
199
+ errorPage() {
200
+ let err = false
201
+ if (import.meta.env.DEV) {
202
+ const requiredPageKeys = ['id', 'activityRef', 'type']
203
+ const errorList = []
204
+ let count = 0
205
+ let requiredTypeValues = [
206
+ 'pg_normal',
207
+ 'pg_menu',
208
+ 'pg_animation',
209
+ 'pg_media',
210
+ 'pg_branch'
211
+ ]
212
+
213
+ requiredPageKeys.forEach((key) => {
214
+ // required key is missing in $data that was passed for the page
215
+ if (!Object.keys(this.pageData).includes(key)) {
216
+ errorList.push(`Missing page ${key} in $data`)
217
+ }
218
+ // Validator value for type
219
+ else if (
220
+ key === 'type' &&
221
+ !requiredTypeValues.includes(this.pageData[key])
222
+ ) {
223
+ errorList.push(`Invalid value assigment for page type in $data`)
224
+ } else if (count < 1) {
225
+ let errString = null
226
+ const requiredValues = ['video', 'audio']
227
+ switch (this.type) {
228
+ // validation for animation page type content
229
+
230
+ case 'pg_animation':
231
+ if (!this.pageData.animation)
232
+ errString = `Missing >>information in $data<< for animation type page`
233
+ if (this.pageData.animation && !this.pageData.animation.refName)
234
+ errString = `Missing >>refName<< for your animation `
235
+ if (errString) {
236
+ errorList.push(errString)
237
+ console.warn(
238
+ `%c WARNING!>>> PAGE: ${errString}`,
239
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
240
+ )
241
+ }
242
+ break
243
+ // valdation for media page type content
244
+ case 'pg_media':
245
+ if (
246
+ !this.pageData.mediaData ||
247
+ Object.keys(this.pageData.mediaData).length < 1
248
+ ) {
249
+ errString = `Missing >>media information<< for media type page`
250
+ errorList.push(errString)
251
+ console.warn(
252
+ `%c WARNING!>>> PAGE: ${errString}`,
253
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
254
+ )
255
+ } else {
256
+ // validation for mediaData content. Must have mType && mSource
257
+ const { mType, mSources, mSubtitle, mTranscript, mPoster } =
258
+ this.pageData.mediaData
259
+ if (!mType || !mSources) {
260
+ errString = `Missing key(s) in mediaData declaration for media`
261
+ errorList.push(errString)
262
+ console.warn(
263
+ `%c WARNING!>>> PAGE: ${errString}`,
264
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
265
+ )
266
+ }
267
+ // Validation for mType content. Must be video/audio
268
+ else if (!requiredValues.includes(mType)) {
269
+ errString = `Invalid declariation for media type must be audio or video`
270
+ errorList.push(errString)
271
+ console.warn(
272
+ `%c WARNING!>>> PAGE: ${errString}`,
273
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
274
+ )
275
+ }
276
+ // Validation for mSource content. Must have at least 1 media source definition
277
+ else if (mSources && mSources.length < 1) {
278
+ errString = `Missing>>information for media source(s)<< for media type page`
279
+ errorList.push(errString)
280
+ console.warn(
281
+ `%c WARNING!>>> PAGE: ${errString}`,
282
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
283
+ )
284
+ }
285
+ //Start validating that the files exsist
286
+ if (!errString) {
287
+ // start validating that the file for subtitle exist in medias folder
288
+ if (mSubtitle && mSubtitle.src) return
289
+
290
+ //validate that mPoster file is present in media file when set
291
+ if (mPoster) {
292
+ if (mPoster.constructor === String) return
293
+ else {
294
+ let errStringInconsole =
295
+ '\n 💥 Invalid type declaration for mPoster.\n 🚩 Must be of type {String}'
296
+
297
+ errString = `l'Attribut 👉 mPoster 👈 pour le media doit être de type {String}`
298
+
299
+ errorList.push(errString)
300
+ console.warn(
301
+ `%c WARNING!>>> PAGE: ${errStringInconsole}`,
302
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
303
+ )
304
+ }
305
+ if (errString) break
306
+ }
307
+
308
+ //validate that transcript file is present in media file when set
309
+ if (mTranscript) {
310
+ if (mTranscript.constructor === String) return
311
+ else {
312
+ let errStringInconsole =
313
+ '\n 💥 Invalid type declaration for mTtranscript.\n 🚩 Must be of type {String}'
314
+
315
+ errString = `l'Attribut 👉 mTranscript 👈 pour le media doit être de type {String}`
316
+
317
+ errorList.push(errString)
318
+ console.warn(
319
+ `%c WARNING!>>> PAGE: ${errStringInconsole}`,
320
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
321
+ )
322
+ }
323
+ if (errString) break
324
+ }
325
+ }
326
+ }
327
+ break
328
+ }
329
+ count++
330
+ }
331
+ })
332
+ if (errorList.length > 0) err = errorList
333
+ }
334
+ return err
335
+ },
336
+
337
+ pgNumber() {
338
+ if (!this.pageData.id) return
339
+ let n = parseInt(this.pageData.id.replace('P', ''))
340
+ return n
341
+ },
342
+
343
+ A11yPageInfo() {
344
+ if (!this.$route || !this.$route.path) return ''
345
+
346
+ let A11YTxt = ''
347
+ const path = this.$route.path
348
+ let reg = /[/|-]/g
349
+
350
+ A11YTxt = path.replaceAll(reg, ' ') // replace all '/' and '-' by space
351
+
352
+ if (A11YTxt.includes('activite'))
353
+ A11YTxt = A11YTxt.replace('activite', this.$t('text.activity'))
354
+
355
+ return A11YTxt.toLowerCase()
356
+ },
357
+
358
+ getDebugModeInfo() {
359
+ const allActivitiesState = JSON.parse(
360
+ JSON.stringify(this.getAllActivitiesState)
361
+ )
362
+ let size = allActivitiesState[this.pageData.activityRef]
363
+ ? allActivitiesState[this.pageData.activityRef].size
364
+ : 0
365
+
366
+ let Pprogress = allActivitiesState[this.pageData.activityRef].progressions
367
+
368
+ let info = {
369
+ id: this.pageData.activityRef,
370
+ size: size,
371
+ progression: Pprogress
372
+ }
373
+
374
+ return info
375
+ }
376
+ },
377
+ watch: {
378
+ userInteraction: {
379
+ async handler(newValue) {
380
+ /**
381
+ * Observe changes in the number of poperties to dispatch updates in the userdata in the Store
382
+ */
383
+
384
+ if (newValue && Object.entries(this.userInteraction).length) {
385
+ await this.store.updateUserMetaData({
386
+ activityRef: this.pageData.activityRef,
387
+ id: this.pageData.id,
388
+ userInteraction: { ...this.userInteraction }
389
+ })
390
+ }
391
+ },
392
+ immediate: true,
393
+ deep: true
394
+ },
395
+ 'store.userDataLoaded': {
396
+ async handler() {
397
+ if (!this.store.userDataLoaded) return
398
+ this.userInteraction = await this.setInitialInteraction()
399
+ },
400
+ immediate: true,
401
+ deep: true
402
+ }
403
+ },
404
+ created() {
405
+ /*
406
+ * Create a custome object for this page that will be added in the collection
407
+ * will greate an id for the page
408
+ * will create a data for the page. Data key can hold media info such as timeline of animation, url, media type
409
+ * will update the store information for the currentPage
410
+ * wil update the store information for the currentTimeline (GSap animation)
411
+ */
412
+
413
+ if (this.pageData && this.errorPage === false) {
414
+ // Handeling presence of animation in the page
415
+ if (this.pageData.animation) {
416
+ // update the store with the information of currentTimeline
417
+ }
418
+
419
+ if (this.type == 'pg_branch') {
420
+ // Update the store with the current page information when branching page is created
421
+ // Note: Branching doesn't trigger router navigation so is done directly here
422
+ this.store.updateCurrentPage({
423
+ activity_Id: this.pageData.activityRef,
424
+ page_Id: this.pageData.id
425
+ })
426
+ this.$bus.$on('branch-page-viewed', this.completePageBranching)
427
+ }
428
+ }
429
+
430
+ if (this.isBranchingPage) this.updateCurrentBranching(this.$data)
431
+ this.$bus.$on('media-viewed', this.setMediaViewed)
432
+ this.$bus.$on('manage-media-players', this.managePlayingMedia)
433
+ this.$bus.$on('video-transcript-toggle', this.onVideoTranscriptToggle)
434
+ this.$bus.$on('save-quiz-answers', this.saveQuizAnswers)
435
+ },
436
+ mounted() {
437
+ //Fix for firefox not updating aria-labelledby (was stuck saying Activite 1, page 1 on every pages)
438
+ this.$refs['page_info'].innerHTML = this.A11yPageInfo
439
+
440
+ // scrolling to top of only when is normal page
441
+ if (this.pageData && this.type === 'pg_normal')
442
+ this.$bus.$emit('move-to-target', 'page_info_section')
443
+
444
+ // set the state of the page when page is mounted : started or completed
445
+ if (this.userInteraction && this.userInteraction.state)
446
+ this.state = this.userInteraction.state
447
+ else if (
448
+ this.type === 'pg_menu' &&
449
+ this.userInteraction.state !== 'completed'
450
+ ) {
451
+ this.state = 'completed' // set menu page state to completed
452
+ } else this.state = 'started' // set the default state to started
453
+
454
+ this.userInteraction['state'] = this.state // add the state to the userInteraction
455
+ // handle completion status of the page.
456
+ if (
457
+ document.documentElement.scrollHeight <=
458
+ document.documentElement.clientHeight + 20 &&
459
+ this.type !== 'pg_branch'
460
+ ) {
461
+ this.completePage()
462
+ }
463
+
464
+ if (this.type === 'pg_branch') {
465
+ this.updateCurrentBranchPage(this.$data)
466
+ }
467
+
468
+ if (this.type == 'pg_branch') return //do not proceed to add listener when branch page
469
+
470
+ window.addEventListener('scroll', this.onFirstScroll, { once: true }) //listener is removed when event fired
471
+ },
472
+ unmounted() {
473
+ //cleaning up all the event listener
474
+ this.$bus.$off('branch-page-viewed', this.completePageBranching)
475
+ this.$bus.$off('media-viewed', this.setMediaViewed)
476
+ this.$bus.$off('manage-media-players', this.managePlayingMedia)
477
+ this.$bus.$off('video-transcript-toggle', this.onVideoTranscriptToggle)
478
+ if (!this.isBranchingPage && this.type !== 'pg_branch')
479
+ this.updateCurrentBranching(null) //unset current branching when leaving a branching page
480
+ if (this.type == 'pg_branch') this.updateCurrentBranchPage(null) //unset current branch page when leaving a branch page
481
+ this.$bus.$off('save-quiz-answers', this.saveQuizAnswers)
482
+ window.removeEventListener('scroll', this.onFirstScroll, { once: true }) //in case user did not scroll
483
+ window.removeEventListener('scroll', this.handleScroll)
484
+ },
485
+ methods: {
486
+ ...mapActions(useAppStore, ['updateCurrentBranchPage']),
487
+ //==============================================================
488
+ onFirstScroll() {
489
+ window.addEventListener('scroll', this.handleScroll)
490
+ },
491
+ /**
492
+ * @description to handle the complete state for the gauge
493
+ */
494
+
495
+ handleScroll(event) {
496
+ event
497
+ /*
498
+ * DocumentElement properties does not alway work properly on all Browser.
499
+ * To Ensure reliable value of its properties on all.
500
+ * Browser we will calculate the Document Height by taking the maximum of
501
+ * body and documentElement height poperties.
502
+ * ref:https://javascript.info/size-and-scroll-window
503
+ */
504
+
505
+ let scrollHeight = null
506
+ let clientHeight = null
507
+ let scrollTop = null
508
+ scrollHeight = Math.max(
509
+ document.body.scrollHeight,
510
+ document.documentElement.scrollHeight,
511
+ document.body.offsetHeight,
512
+ document.documentElement.offsetHeight,
513
+ document.body.clientHeight,
514
+ document.documentElement.clientHeight
515
+ )
516
+ clientHeight = document.documentElement.clientHeight
517
+ scrollTop = window.scrollY
518
+
519
+ // //Set scroll limit reached at 150px above the document height.
520
+ let scrollLimit = scrollHeight - 150
521
+ let fullyScrolled = Math.round(clientHeight + scrollTop)
522
+
523
+ //consider page completed when scolled value has reached or passed set limit
524
+ if (fullyScrolled >= scrollLimit && this.state !== 'completed') {
525
+ this.completePage()
526
+ }
527
+ },
528
+ /**
529
+ * @description set the state of the page to complete
530
+ * @fires send-xapi-statement to AppBaseModule.vue
531
+ */
532
+ completePage() {
533
+ if (
534
+ ['pg_menu', 'pg_branch'].includes(this.type) ||
535
+ this.state == 'completed' ||
536
+ this.isBranchingPage
537
+ )
538
+ return
539
+
540
+ this.state = 'completed'
541
+ this.userInteraction.state = this.state
542
+ },
543
+
544
+ /**
545
+ * @description set the state of the branching page to complete
546
+ * @fires send-xapi-statement to AppBaseModule.vue
547
+ */
548
+ completePageBranching() {
549
+ //Get the current branching from the store
550
+ const currentBranching = this.getCurrentBranching
551
+ if (!currentBranching || currentBranching.id !== this.$route.meta.id)
552
+ return
553
+ if (currentBranching.state === 'completed') return
554
+
555
+ const children = this.$route.meta.children
556
+ let count = 0
557
+ children.forEach((c) => {
558
+ let progress = this.getProgress(c._ref)
559
+
560
+ if (progress.state == 'completed') count += 1
561
+ })
562
+
563
+ if (count !== children.length) return
564
+
565
+ currentBranching.state = 'completed' //set the state of the page to completed
566
+ currentBranching.userInteraction.state = currentBranching.state // update the useInteraction state
567
+ },
568
+ /**
569
+ * @description Get the user progress for the current page
570
+ * @param {string} id (Otpional) - the id of the targeted page
571
+ * @return {Oject} - the existing user data for the current page
572
+ */
573
+ getProgress(id) {
574
+ id = id || this.pageData.id
575
+
576
+ const record = this.getPageInteraction(this.pageData.activityRef, id)
577
+
578
+ if (Object.entries(record).length) {
579
+ const { userInteraction } = record
580
+ return userInteraction
581
+ }
582
+ return {}
583
+ },
584
+
585
+ anchorProgress() {
586
+ const anchors = document.querySelectorAll('.anchor') // look for anchor
587
+ const options = {
588
+ root: null,
589
+ threshold: 0
590
+ }
591
+
592
+ let anchorsComplete
593
+ let indexStrt
594
+ let anchorString
595
+ let indexEnd
596
+ let anchorComplete
597
+
598
+ let target = document.querySelector('#App-base')
599
+ // get anchor already seen
600
+ anchorsComplete = this.getAnchorComplete()
601
+
602
+ const observer = new IntersectionObserver((entries) => {
603
+ // everytime the page passes a anchor
604
+ observer.observe(target)
605
+
606
+ entries.forEach((entry) => {
607
+ // when it's visable in the page
608
+
609
+ if (entry.isIntersecting) {
610
+ // get the target
611
+
612
+ this.anchorInfo = entry.target.classList
613
+ indexStrt = this.anchorInfo.value.indexOf('anchor-')
614
+ // work the string to get juste the anchor tag
615
+ // must be the same as the class
616
+ if (indexStrt == -1) {
617
+ return
618
+ }
619
+
620
+ anchorString = this.anchorInfo.value.substring(indexStrt)
621
+
622
+ indexEnd = anchorString.indexOf(' ')
623
+ if (indexEnd != -1) {
624
+ anchorComplete = anchorString.slice(0, indexEnd)
625
+ } else {
626
+ anchorComplete = anchorString
627
+ }
628
+
629
+ //get all the anchors of the current activity
630
+ const anchors_list = this.getAnchorsForActivity(
631
+ this.pageData.activityRef
632
+ )
633
+
634
+ //search for the current ancor
635
+ const anc = anchors_list.find((a) => a.anchorTag === anchorComplete)
636
+ //dispatch the current anchor to the store
637
+ if (anc) {
638
+ // update the store value for current section
639
+ // this.updateCurrentSection(anc)
640
+ // Ask bread scrumb to update its information
641
+
642
+ this.$bus.$emit('anchor-seen', anchorComplete)
643
+ } else {
644
+ if (import.meta.env.DEV)
645
+ console.warn(
646
+ `%c WARNING!>>> Anchor handeling: 👉${anchorComplete}👈 doesn't exist. Please provide a valid anchor.`,
647
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
648
+ )
649
+
650
+ return
651
+ }
652
+
653
+ // if you didn't aldreay seen anchors
654
+ if (anchorsComplete != undefined) {
655
+ // look if you already saw this anchor
656
+ if (anc && !anchorsComplete.includes(anchorComplete)) {
657
+ // push it in the array them in the store
658
+ anchorsComplete.push(anchorComplete)
659
+ this.$set(this.userInteraction, 'anchors', anchorsComplete)
660
+ }
661
+ } else {
662
+ // if you never saw any anchor
663
+ // push it in the array them in the store
664
+ anchorsComplete = []
665
+ if (anc) anchorsComplete.push(anchorComplete)
666
+ this.$set(this.userInteraction, 'anchors', anchorsComplete)
667
+ }
668
+ }
669
+ })
670
+ }, options)
671
+
672
+ // observer call llok for each anchor in page
673
+ anchors.forEach((anchor) => {
674
+ observer.observe(anchor)
675
+ })
676
+ },
677
+ getAnchorComplete() {
678
+ const records = this.getPageInteraction(
679
+ this.pageData.activityRef,
680
+ this.pageData.id
681
+ )
682
+
683
+ // Verify if the element existe
684
+ if (
685
+ records &&
686
+ records.userInteraction &&
687
+ records.userInteraction.anchors
688
+ ) {
689
+ // if you already saw anchors return them
690
+
691
+ return records.userInteraction.anchors
692
+ }
693
+ },
694
+ showChoiceBif(data) {
695
+ // check if you made a choice
696
+ if (
697
+ typeof this.getBifChoice === 'undefined' ||
698
+ Object.keys(this.getBifChoice).length === 0
699
+ ) {
700
+ return data['A']
701
+ } else {
702
+ if (this.getBifChoice.choix) {
703
+ // get the choise from store
704
+ if (data.hasOwnProperty(this.getBifChoice.choix)) {
705
+ let choice = data[this.getBifChoice.choix]
706
+ //return choice
707
+ return choice
708
+ }
709
+ }
710
+ }
711
+ },
712
+ openPopup(data) {
713
+ this.$bus.$emit('open-popup', data)
714
+ },
715
+ /**
716
+ * @description Send to the bd that the media have been view by the user- Add the id of the media to the
717
+ * userInteraction of the page
718
+ */
719
+ setMediaViewed(mediaID) {
720
+ // Should get the userData to check if current mediaElement has entry
721
+ let { mediasViewed } = this.userInteraction
722
+
723
+ // Should create entry for medias viewed in userInteraction if none
724
+ if (!mediasViewed) {
725
+ mediasViewed = []
726
+
727
+ this.userInteraction.mediasViewed = mediasViewed
728
+ }
729
+
730
+ //Should add ID in media viewed list if viewed for the 1st time
731
+ if (mediasViewed.includes(mediaID)) return
732
+
733
+ mediasViewed.push(mediaID)
734
+
735
+ this.userInteraction.mediasViewed = mediasViewed //Update the userInteraction data of the page
736
+ }, //
737
+
738
+ /**
739
+ * @description - Method to manage the playing state of media element in the page.
740
+ * Only one media should play at a time. When receives signal of new media playing
741
+ * get the reference of the previous media in play from the store and
742
+ * put it in stop state. Also closes the transcript sidebar if it is open.
743
+ * @param {HTMLElement} media - the actual media that is playing
744
+ */
745
+ managePlayingMedia(media) {
746
+ if (!media) return
747
+ //Should get all the media of the page from store
748
+ const { mElements } = this.getCurrentPage
749
+ if (!mElements || !mElements.length) return
750
+
751
+ // // Should stop any media playing
752
+ mElements.forEach((m) => {
753
+ if (m.id == media.id) return
754
+
755
+ const attrKeys = Object.keys(m) //
756
+ const playbarInstance = m[attrKeys[3]]
757
+ const HTMLmediaElement = m[attrKeys[1]]
758
+
759
+ //Close the transcript sidebar if it is open
760
+ if (playbarInstance.transcriptEnabled) {
761
+ playbarInstance.toggleViewTranscript()
762
+ }
763
+ //Check if the media is playing to stop it. Playing state is given by the instance of the target play-bar
764
+ if (playbarInstance.isPlaying) {
765
+ HTMLmediaElement.pause() // target the HTMLmediaElement to control it state
766
+ playbarInstance.isPlaying = false //change this isPlaying value of the instance
767
+
768
+ playbarInstance.timer.pause() // pause the timer of the instance
769
+ }
770
+ })
771
+ },
772
+ /**
773
+ * @description - Method to manage the state of the transcript and fullscreen buttons.
774
+ * Disables those buttons on other media playbar when a transcript sidebar is open.
775
+ *
776
+ * @param {HTMLElement} media - the actual media that is playing
777
+ * @param {Boolean} transcriptShown - current status of the transcript sidebar
778
+ */
779
+ onVideoTranscriptToggle(media, transcriptShown) {
780
+ if (!media) return
781
+ //Should get all the media of the page from store
782
+ const { mElements } = this.getCurrentPage
783
+ if (!mElements || !mElements.length) return
784
+ mElements.forEach((m) => {
785
+ if (m.id == media.id) return
786
+ const attrKeys = Object.keys(m)
787
+ const playbarInstance = m[attrKeys[3]]
788
+ playbarInstance.otherVideoTranscriptShown = transcriptShown
789
+ })
790
+ },
791
+ setInitialInteraction() {
792
+ const { activityRef, id: pageID } = this.pageData
793
+ const { userInteraction: previousInteraction = {} } =
794
+ this.getPageInteraction(activityRef, pageID)
795
+
796
+ return previousInteraction
797
+ },
798
+ saveQuizAnswers(el, quiz) {
799
+ if (!this.userInteraction.quizAnswers)
800
+ return (this.userInteraction.quizAnswers = { ...quiz })
801
+ const quizID = Object.keys(quiz)[0]
802
+
803
+ if (!this.userInteraction.quizAnswers[quizID])
804
+ return (this.userInteraction.quizAnswers = {
805
+ ...this.userInteraction.quizAnswers,
806
+ ...quiz
807
+ })
808
+ const quizValue = Object.values(quiz)[0]
809
+ this.userInteraction.quizAnswers[quizID] = quizValue
810
+ // const {}
811
+ }
812
+ }
813
+ }
814
+ </script>
815
+ <style lang="scss">
816
+ .img-popUp {
817
+ position: relative;
818
+
819
+ .box-trigger {
820
+ position: absolute;
821
+ width: 100%;
822
+ height: 100%;
823
+ top: 0;
824
+ left: 0;
825
+
826
+ .btn {
827
+ position: absolute;
828
+ }
829
+ }
830
+ }
831
+
832
+ .debug-pageInfo {
833
+ position: fixed;
834
+ right: -250px;
835
+ bottom: 20px;
836
+
837
+ color: #333;
838
+ background-color: rgba(#eaabb6b3, 0.9);
839
+
840
+ display: flex;
841
+ flex-direction: row;
842
+
843
+ &.open {
844
+ right: 0;
845
+ }
846
+
847
+ .w-btn {
848
+ padding: 10px;
849
+ height: 100%;
850
+ cursor: pointer;
851
+ background: rgba(#fff, 0.05);
852
+
853
+ &:hover {
854
+ background: rgba(#fff, 0.1);
855
+ }
856
+ }
857
+
858
+ .ctn {
859
+ padding: 24px;
860
+ width: 250px;
861
+
862
+ ul {
863
+ margin-left: 15px;
864
+ }
865
+ }
866
+ }
867
+ </style>