fcad-core-dragon 2.1.1 → 2.2.0-beta.1

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