fcad-core-dragon 2.1.0-beta.4 → 2.1.0

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