fcad-core-dragon 2.0.0-beta.3 → 2.0.0-beta.4

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 (95) hide show
  1. package/.editorconfig +33 -33
  2. package/.eslintignore +29 -29
  3. package/.eslintrc.cjs +81 -81
  4. package/CHANGELOG +373 -364
  5. package/README.md +71 -71
  6. package/bk.scss +117 -117
  7. package/package.json +61 -61
  8. package/src/$locales/en.json +143 -143
  9. package/src/$locales/fr.json +105 -105
  10. package/src/assets/data/onboardingMessages.json +47 -47
  11. package/src/components/AppBase.vue +1147 -1054
  12. package/src/components/AppBaseButton.vue +87 -87
  13. package/src/components/AppBaseErrorDisplay.vue +438 -438
  14. package/src/components/AppBaseFlipCard.vue +84 -84
  15. package/src/components/AppBaseModule.vue +1636 -1673
  16. package/src/components/AppBasePage.vue +779 -779
  17. package/src/components/AppBasePopover.vue +41 -41
  18. package/src/components/AppCompAudio.vue +234 -234
  19. package/src/components/AppCompBranchButtons.vue +552 -552
  20. package/src/components/AppCompButtonProgress.vue +126 -126
  21. package/src/components/AppCompCarousel.vue +298 -298
  22. package/src/components/AppCompInputCheckBoxNext.vue +195 -195
  23. package/src/components/AppCompInputDropdownNext.vue +159 -159
  24. package/src/components/AppCompInputRadioNext.vue +152 -152
  25. package/src/components/AppCompInputTextNext.vue +106 -106
  26. package/src/components/AppCompInputTextTableNext.vue +141 -141
  27. package/src/components/AppCompInputTextToFillDropdownNext.vue +230 -230
  28. package/src/components/AppCompInputTextToFillNext.vue +171 -171
  29. package/src/components/AppCompJauge.vue +74 -74
  30. package/src/components/AppCompMenu.vue +423 -413
  31. package/src/components/AppCompMenuItem.vue +228 -228
  32. package/src/components/AppCompNavigation.vue +959 -960
  33. package/src/components/AppCompNoteCall.vue +133 -133
  34. package/src/components/AppCompNoteCredit.vue +292 -292
  35. package/src/components/AppCompPlayBar.vue +1218 -1218
  36. package/src/components/AppCompPlayBarNext.vue +2052 -2052
  37. package/src/components/AppCompPlayBarProgress.vue +82 -82
  38. package/src/components/AppCompPopUpNext.vue +503 -503
  39. package/src/components/AppCompQuizNext.vue +2904 -2904
  40. package/src/components/AppCompQuizRecall.vue +276 -276
  41. package/src/components/AppCompSVGNext.vue +347 -347
  42. package/src/components/AppCompSettingsMenu.vue +172 -172
  43. package/src/components/AppCompTableOfContent.vue +387 -387
  44. package/src/components/AppCompTranscript.vue +24 -24
  45. package/src/components/AppCompVideoPlayer.vue +368 -368
  46. package/src/components/AppCompViewDisplay.vue +6 -6
  47. package/src/components/BaseModule.vue +72 -72
  48. package/src/composables/useQuiz.js +206 -206
  49. package/src/externalComps/ModuleView.vue +22 -22
  50. package/src/externalComps/SummaryView.vue +91 -91
  51. package/src/main.js +272 -272
  52. package/src/mixins/$mediaMixins.js +819 -819
  53. package/src/mixins/timerMixin.js +155 -155
  54. package/src/module/stores/appStore.js +901 -893
  55. package/src/module/xapi/ADL.js +380 -376
  56. package/src/module/xapi/Crypto/Hasher.js +241 -241
  57. package/src/module/xapi/Crypto/WordArray.js +278 -278
  58. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  59. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  60. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  61. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  62. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  63. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  64. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  65. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  66. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  67. package/src/module/xapi/Crypto/index.js +53 -53
  68. package/src/module/xapi/Statement/activity.js +47 -47
  69. package/src/module/xapi/Statement/agent.js +55 -55
  70. package/src/module/xapi/Statement/group.js +26 -26
  71. package/src/module/xapi/Statement/index.js +259 -259
  72. package/src/module/xapi/Statement/statement.js +253 -253
  73. package/src/module/xapi/Statement/statementRef.js +23 -23
  74. package/src/module/xapi/Statement/substatement.js +22 -22
  75. package/src/module/xapi/Statement/verb.js +36 -36
  76. package/src/module/xapi/activitytypes.js +17 -17
  77. package/src/module/xapi/launch.js +157 -157
  78. package/src/module/xapi/utils.js +167 -167
  79. package/src/module/xapi/verbs.js +294 -294
  80. package/src/module/xapi/wrapper.js +1963 -1963
  81. package/src/module/xapi/xapiStatement.js +444 -444
  82. package/src/plugins/bus.js +8 -8
  83. package/src/plugins/gsap.js +14 -14
  84. package/src/plugins/helper.js +314 -308
  85. package/src/plugins/i18n.js +44 -44
  86. package/src/plugins/idb.js +227 -219
  87. package/src/plugins/save.js +37 -37
  88. package/src/plugins/scorm.js +287 -287
  89. package/src/plugins/xapi.js +11 -11
  90. package/src/public/index.html +33 -33
  91. package/src/router/index.js +43 -43
  92. package/src/router/routes.js +312 -312
  93. package/src/shared/generalfuncs.js +210 -210
  94. package/src/shared/validators.js +1069 -1069
  95. package/vite.config.js +0 -27
@@ -1,1673 +1,1636 @@
1
- <!--
2
- *@ Description: This component is used as main container to display application and containt to display
3
- *@ What it does: The component fetch the data for the page to display on navigation.
4
- *
5
- *@Note :Must be used
6
- -->
7
-
8
- <template>
9
- <div fluid class="module">
10
- <div
11
- id="page_info_section"
12
- aria-labelledby="page_info"
13
- aria-live="true"
14
- ></div>
15
- <a class="skip-link" href="" @click.prevent="skipToMain">
16
- {{ $t('message.skip_content') }}
17
- </a>
18
-
19
- <nav
20
- v-show="!isMenu"
21
- id="navTool"
22
- :key="$route.fullpath"
23
- class="app-nav"
24
- :class="{ show: closeDelay }"
25
- >
26
- <!------------------------ Nav for drMode ------------------>
27
- <!-- <app-comp-table-of-content /> -->
28
- <!------------------------ Nav for FullMode ---------------->
29
-
30
- <app-comp-navigation
31
- :app-status="appReady ? true : null"
32
- :auto-navigate="theNavigationBetweenActivity"
33
- />
34
- </nav>
35
- <base-module :m-data="$data">
36
- <v-container
37
- id="wrapper-content"
38
- fluid
39
- :class="{ active: moduleConfig.videoFull }"
40
- class="scroll-bar"
41
- >
42
- <div class="box">
43
- <router-view ref="main" :key="$route.fullPath" />
44
- <!-- <router-view v-show="appReady" ref="main" :key="$route.fullPath" /> -->
45
- </div>
46
-
47
- <div id="primary_nav_wrapper"></div>
48
- </v-container>
49
- </base-module>
50
-
51
- <!------------------POPUP QUICK -------------------------->
52
- <app-comp-pop-up-next v-show="popupIsOpen">
53
- <!-- <template #content></template> -->
54
- </app-comp-pop-up-next>
55
- <!------------------END USE POP UP-------------------------->
56
-
57
- <!--------------RIGHT SIDEBAR (for display of extra contents)------------>
58
- <Transition name="right-sidebar-transition" mode="out-in">
59
- <div
60
- v-if="rightSidebarVisible"
61
- id="right-sidebar"
62
- ref="right-sidebar"
63
- :key="dynamicSidebarContent._id"
64
- :class="{
65
- 'v-media': dynamicSidebarContent._context === 'ctxTranscript'
66
- // 'right-sidebar-show': rightSidebarVisible
67
- }"
68
- :aria-label="dynamicSidebarContent._label"
69
- >
70
- <div id="right-sidebar-header">
71
- <button
72
- class="btn-reserve-ico embranchement-close"
73
- @click="
74
- closeSidebar(
75
- dynamicSidebarContent._context,
76
- dynamicSidebarContent._container
77
- ? dynamicSidebarContent._container
78
- : null
79
- )
80
- "
81
- >
82
- <svg>
83
- <use href="#close-square-icon" />
84
- </svg>
85
- <span class="sr-only">{{ $t('button.closePopUp') }}</span>
86
- </button>
87
- </div>
88
-
89
- <div id="right-sidebar-body">
90
- <component
91
- :is="dynamicSidebarContent._component"
92
- class="v-media"
93
- v-bind="{ ...dynamicSidebarContent._comProps }"
94
- />
95
- <!-- </Transition> -->
96
- </div>
97
- <div id="right-sidebar-footer"></div>
98
- </div>
99
- </Transition>
100
- <footer></footer>
101
- <!-------------------------END RIGHT SIDEBAR------------------------------->
102
- </div>
103
- </template>
104
- <script>
105
- import { mapState, mapActions } from 'pinia'
106
- import { useAppStore } from '../module/stores/appStore'
107
- import BaseModule from './BaseModule.vue'
108
- import { timerMixin } from '../mixins/timerMixin'
109
- import AppCompTranscript from './AppCompTranscript.vue'
110
- import { defineAsyncComponent } from 'vue'
111
- //import { fileAssets } from '../shared/generalfuncs'
112
- //import
113
- export default {
114
- components: {
115
- BaseModule,
116
- AppCompTranscript
117
- },
118
- mixins: [timerMixin],
119
- props: {
120
- moduleConfig: {
121
- type: Object,
122
- default: () => {
123
- return {
124
- allowNavigationToActivity: false, // set Previous/Next can allow navigation between activities. Set to false if do not want navigation between activities with Previous/Next
125
- main: ''
126
- }
127
- }
128
- }
129
- },
130
- data() {
131
- return {
132
- meta: {},
133
- videoFull: null,
134
- routeData: [],
135
- changePage: false,
136
- randKey: Math.floor(Math.random() * 10001),
137
- popupIsOpen: false,
138
- hidePlayBar: false, // Controle visibility of the play bar. set to true to hide play bar
139
- stmt: null, // holder for xapi statememt,
140
- moduleTimer: 0, //tracker for overall time spent the lesson
141
- activityTimer: 0, // tracker for the time spent on activity,
142
- routeChangeCounter: 0,
143
- toolTipTarget: '', //for the tool tip,
144
- onboardingMessages: {}, //for the onboarding @todo replace with default file
145
- settingsSelected: {},
146
- compID: null,
147
- rightSidebarVisible: false,
148
- lastInFocus: null,
149
- closeDelay: false,
150
- timeOut: null,
151
- infocusTabIndex: null,
152
- transcriptVisible: false,
153
- transcriptContent: null,
154
- transcriptContainer: null,
155
- branchingVisible: false,
156
- lessonCompletionStatus: false,
157
- checkedDataFromServer: 0
158
- }
159
- },
160
- computed: {
161
- ...mapState(useAppStore, [
162
- 'getCurrentPage',
163
- 'getCurrentBranchPage',
164
- 'getAppStatus',
165
- 'getUserInteraction',
166
- 'getModuleInfo',
167
- 'getAllActivities',
168
- 'getAllCompleted',
169
- 'getConnectionInfo',
170
- 'hasMediaElOrTimeline',
171
- 'getMenuSettings',
172
- 'getRouteHistory',
173
- 'getOnboardingEnabled',
174
- 'getApplicationSettings',
175
- 'getDataFromServer',
176
- 'getCompStatusTracker'
177
- ]),
178
- isMenu() {
179
- return this.$route.name === 'menu'
180
- },
181
- appReady() {
182
- return this.getAppStatus === 'ready' ? true : false
183
- },
184
-
185
- hasMedia() {
186
- return typeof this.hasMediaElOrTimeline === 'object'
187
- },
188
- activityHasChanged() {
189
- const rd = this.routeData.toReversed()
190
-
191
- if (
192
- rd.length <= 1 ||
193
- rd[1].activity_ref !== this.$route.meta.activity_ref
194
- )
195
- return true
196
- else return false
197
- },
198
-
199
- /**
200
- * @description Set the id the module
201
- */
202
- theId() {
203
- let id = 'mod_001'
204
- if (this.moduleConfig.id) id = this.moduleConfig.id
205
- return id
206
- },
207
- /**
208
- * @description Set the title of the lesson
209
- */
210
- theTitle() {
211
- let title = this.$t(`text.place_holder.for_lesson_title`)
212
-
213
- if (this.getMenuSettings.lessonTitle)
214
- title = this.getMenuSettings.lessonTitle
215
- return title
216
- },
217
- /**
218
- * @description Set desciption for the module
219
- *
220
- */
221
- theDescription() {
222
- let description = null
223
-
224
- if (this.moduleConfig.description)
225
- description = this.moduleConfig.description
226
- return description
227
- },
228
- /**
229
- * @description Set default value for bookmark
230
- */
231
- bookmarkActive() {
232
- let bookmarkState
233
-
234
- if (this.moduleConfig.bookmarkActive)
235
- bookmarkState = this.moduleConfig.bookmarkActive
236
- else bookmarkState = false
237
- return bookmarkState
238
- },
239
- /**
240
- * @description set Previous/Next can allow navigation between activities.
241
- *
242
- */
243
- theNavigationBetweenActivity() {
244
- let navBwteenActivity = false
245
- if (this.moduleConfig.allowNavigationToActivity)
246
- navBwteenActivity = this.moduleConfig.allowNavigationToActivity
247
- return navBwteenActivity
248
- },
249
- /**
250
- * @description Control use INTRODUCTION page in the Lesson.
251
- * set to false if there is no introduction
252
- */
253
- theIntroIsActivated() {
254
- let introActive = false
255
- if (this.moduleConfig.introActive)
256
- introActive = this.moduleConfig.introActive
257
- return introActive
258
- },
259
- isMain() {
260
- const { main } = this.moduleConfig
261
- if (!main || main === ' ') return this.$route.meta.id
262
-
263
- let mainEl = document.querySelector(`#${main}`)
264
-
265
- if (!mainEl) return this.$route.meta.id
266
-
267
- return mainEl
268
- },
269
-
270
- dynamicSidebarContent() {
271
- if (!this.transcriptVisible && !this.branchingVisible) return null
272
- let sidebarSettings = {}
273
- let _label = null
274
-
275
- if (this.transcriptVisible) {
276
- _label =
277
- this.$i18n.locale === 'fr'
278
- ? 'Contenu de la transcription'
279
- : 'Content of the transcript'
280
-
281
- sidebarSettings = {
282
- _component: AppCompTranscript,
283
- _comProps: {
284
- content: this.transcriptContent
285
- },
286
- _context: 'ctxTranscript',
287
- _container: this.transcriptContainer,
288
- _label,
289
- _id: 'transcript'
290
- }
291
-
292
- //console.log(sidebarSettings)
293
- } else if (this.branchingVisible) {
294
- if (this.$route.meta.type !== 'branching' || !this.compID) return null
295
-
296
- const componentName = this.compID
297
- const { activityRef } = this.getCurrentPage //get activity id from current page
298
- _label =
299
- this.$i18n.locale === 'fr'
300
- ? "contenu de l'embranchement"
301
- : 'content of the branching'
302
-
303
- sidebarSettings = {
304
- _component: defineAsyncComponent(
305
- () => import(`@/module/${activityRef}/${componentName}.vue`)
306
- ),
307
- _comProps: false,
308
- _context: 'ctxBranching',
309
- _label,
310
- _id: componentName
311
- }
312
- }
313
- return sidebarSettings
314
- },
315
-
316
- navigationHistory() {
317
- return this.getRouteHistory
318
- }
319
- },
320
- watch: {
321
- navigationHistory: {
322
- handler() {
323
- this.routeData = this.navigationHistory
324
- },
325
- deep: true,
326
- immediate: true
327
- },
328
- $route: {
329
- handler() {
330
- this.getFocusables().then((r) => {
331
- //Pressing Tab or Shit + tab should make it possible to cycle focus through them
332
- if (!r) return
333
- r[0].focus()
334
- this.infocusTabIndex = r[0]
335
- })
336
-
337
- //update the routeChangeCounter when navigation
338
- if (this.routeChangeCounter < 3) this.routeChangeCounter += 1
339
-
340
- /*
341
- *Start Timer on New activities
342
- */
343
-
344
- const trackedRouteType = ['introduction', 'conclusion', 'normal']
345
- if (trackedRouteType.includes(this.$route.meta.type)) {
346
- if (this.$route.name.includes('.')) return
347
-
348
- //Start the timer every time that there is a new activity
349
- if (this.timerState === 'started') this.stopTimer('activity') // reset the activity timer
350
- this.startTimer('activity')
351
-
352
- //Send statement when activity as changed
353
- if (this.activityHasChanged)
354
- this.sendStartStatement({ id: this.$route.meta.activity_ref })
355
- }
356
- this.transcriptVisible = false
357
- },
358
- deep: true,
359
- immediate: true
360
- },
361
-
362
- /**
363
- * @description Defined in Store Watch all completed activities to send a lesson completion statement
364
- */
365
- getAllCompleted: {
366
- handler() {
367
- // Check once the server to set the completion status of the lesson
368
- if (this.getDataFromServer && this.checkedDataFromServer < 1) {
369
- const { completedState } = this.getDataFromServer
370
-
371
- this.lessonCompletionStatus =
372
- !completedState || !completedState.completion
373
- ? false
374
- : completedState.completion
375
- this.checkedDataFromServer += 1
376
- }
377
-
378
- if (!this.lessonCompletionStatus) this.sendCompletionStatus('LESSON')
379
- },
380
- deep: true,
381
- immediate: true
382
- },
383
- /**
384
- * @description Defined in timerMixin Watch The epalsed time for autosaving the user reached position
385
- */
386
- elapsedTime: {
387
- handler() {
388
- if (this.timerState !== 'started') return
389
- //send a statement every x time (second)
390
- if (
391
- this.timerState === 'started' &&
392
- this.elapsedTime % 500 === 0 &&
393
- this.getModuleInfo.packageType === 'xapi' &&
394
- this.getConnectionInfo &&
395
- this.getConnectionInfo.actor &&
396
- this.getConnectionInfo.remote &&
397
- this.getDataFromServer
398
- ) {
399
- const { lessonPosition } = this.getDataFromServer
400
-
401
- const lastReached = lessonPosition.length ? lessonPosition[0] : ''
402
-
403
- //only Send savepoint statement when 3 navigation happen and the route is different from last saved
404
- if (
405
- this.routeChangeCounter >= 3 &&
406
- lastReached !== this.$route.name
407
- ) {
408
- let text
409
- //Defining the text to display for stmt description and definition
410
- switch (this.$i18n.locale) {
411
- case 'fr':
412
- if (this.getModuleInfo.courseID)
413
- text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
414
- else text = `Le ${this.getModuleInfo.id}`
415
- break
416
- case 'en':
417
- if (this.getModuleInfo.courseID)
418
- text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
419
- else text = `The ${this.getModuleInfo.id}`
420
- break
421
- }
422
-
423
- //Creating custom statment
424
- const stmt = {
425
- verb: 'progressed',
426
- definition: text,
427
- description: text,
428
- extensions: [
429
- {
430
- id: 'ending-point',
431
- content: this.$route.name
432
- }
433
- ],
434
- duration: this.lessonDuration
435
- }
436
-
437
- this.$bus.$emit('send-xapi-statement', stmt)
438
- this.routeChangeCounter = 0 //reset counter after saving
439
- }
440
- }
441
- },
442
- deep: true
443
- }
444
- },
445
- beforeUnmount() {
446
- //Communication events
447
- this.$bus.$off('close-sidebar', this.closeSidebar)
448
- this.$bus.$off('open-sidebar', this.openSidebar)
449
- this.$bus.$off('open-popup', this.openPopup)
450
- this.$bus.$off('close-popup', this.closePopup)
451
- this.$bus.$off('start-onboarding', this.startOnboarding)
452
- this.$bus.$off('videoFullScreen', this.onVideoFullScreen)
453
- this.$bus.$off('save-to-scorm', this.saveToScorm)
454
- this.$bus.$off('launch-xapi-resource', this.launchResource)
455
- this.$bus.$off('update-route-history', this.updateRouteHistory)
456
- this.$bus.$off('update-content', this.updateContent)
457
- this.$bus.$off('show-transcript', this.openTranscript)
458
- this.$bus.$off('send-completion-event', this.sendCompletionStatus)
459
- this.$bus.$off('send-starting-event', this.sendStartStatement)
460
- //nav mouseleave event
461
- let nav = document.getElementById('navTool')
462
- if (nav) {
463
- nav.removeEventListener('mouseleave', this.onNavMouseleave)
464
- }
465
- //sidebar scroll event
466
- const rightSidebar = this.getRightSidebar()
467
- if (rightSidebar) {
468
- rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
469
- }
470
- //keayboard listener
471
- document.removeEventListener('keydown', this.handleKeyboardControls)
472
- },
473
- created() {
474
- let lessonLabel, lessonNumber, lessonTitle, titleString
475
-
476
- lessonLabel = this.$t('text.lesson')
477
- lessonNumber = this.moduleConfig.id.replace('module_', '')
478
- lessonTitle = this.theTitle
479
- titleString = lessonLabel + ' ' + lessonNumber + ' : ' + lessonTitle
480
-
481
- //Remove prefix for introduction or conclusion, according to isIntroConclu setting in Module.vue
482
- if (typeof this.moduleConfig.isIntroConclu !== 'undefined') {
483
- if (this.moduleConfig.isIntroConclu) {
484
- titleString = lessonTitle
485
- }
486
- }
487
- document.title = titleString
488
-
489
- // Tell the store the state of intro and conclusion
490
- this.updateIntroStatus(this.theIntroIsActivated)
491
- //Communication events
492
- this.$bus.$on('open-popup', this.openPopup)
493
-
494
- this.$bus.$on('close-popup', this.closePopup)
495
-
496
- this.$bus.$on('start-onboarding', this.startOnboarding)
497
-
498
- this.$bus.$on('open-sidebar', this.openSidebar)
499
-
500
- this.$bus.$on('close-sidebar', this.closeSidebar)
501
-
502
- this.$bus.$on('videoFullScreen', this.onVideoFullScreen)
503
-
504
- this.$bus.$on('save-to-scorm', this.saveToScorm)
505
-
506
- this.$bus.$on('launch-xapi-resource', this.launchResource)
507
-
508
- this.$bus.$on('update-route-history', this.updateRouteHistory)
509
- this.$bus.$on('update-content', this.updateContent)
510
- this.$bus.$on('show-transcript', this.openTranscript)
511
- this.$bus.$on('send-completion-event', this.sendCompletionStatus)
512
- this.$bus.$on('send-starting-event', this.sendStartStatement)
513
-
514
- this.initLesson()
515
-
516
- if (this.getRouteHistory.length != 0) {
517
- this.routeHistory = this.getRouteHistory
518
- }
519
- setTimeout(() => {
520
- this.settingsSelected = { ...this.getApplicationSettings }
521
- }, 800)
522
- },
523
- mounted() {
524
- //A11Y: Bring back the focus on the body element after each navigation
525
- document.body.setAttribute('tabindex', -1) //needed to use .focus()
526
- this.$router.afterEach(() => {
527
- document.body.focus()
528
- })
529
-
530
- let nav = document.getElementById('navTool')
531
-
532
- if (nav) nav.addEventListener('mouseleave', this.onNavMouseleave)
533
-
534
- document.addEventListener('keydown', this.handleKeyboardControls)
535
- },
536
- methods: {
537
- ...mapActions(useAppStore, [
538
- 'updateIntroStatus',
539
- 'updateCurrentTimeline',
540
- 'updateCurrentMediaElements',
541
- 'updateCurrentPage',
542
- 'updateDataFetchFromServer'
543
- ]),
544
- onNavMouseleave() {
545
- let widgetOpen = document.getElementsByClassName('open')
546
-
547
- if (widgetOpen.length == 0) {
548
- this.cancelTimeout()
549
- this.closeDelay = true
550
- this.startTimeout()
551
- } else {
552
- this.closeDelay = true
553
- }
554
- },
555
- onVideoFullScreen(value) {
556
- this.videoFull = value
557
- },
558
- /* Get All Element related to the media return a list of all DOM element*/
559
- async getFocusables() {
560
- return new Promise(function (resolve, reject) {
561
- setTimeout(function () {
562
- const getAllFocusables = () => {
563
- const listItems = document.querySelectorAll('.v-media')
564
- if (!listItems || !listItems.length) return null
565
- return Array.from(listItems)
566
- }
567
- resolve(getAllFocusables())
568
- }, 200)
569
- })
570
- },
571
- /**
572
- * @description Handle Keyboard controls
573
- * @summary
574
- * @param {Object} evt - event that fired the action
575
- *
576
- */
577
- async handleKeyboardControls(evt) {
578
- let { code } = evt
579
-
580
- if (code === 'Escape' && this.rightSidebarVisible)
581
- return (this.rightSidebarVisible = false)
582
-
583
- //==========================================
584
- /*const videoRange = await this.getFocusables()
585
- if (videoRange && videoRange.indexOf(document.activeElement) === -1)
586
- return
587
- this.$bus.$emit('play-media', code)*/
588
- },
589
-
590
- /**
591
- * @description open the right sidebar component and show and set it content
592
- * @summary opens sidebar according to context
593
- * @param {Object} obj {ctx , e} - context and content to display in the sidebar
594
- *
595
- */
596
- openSidebar(obj) {
597
- if (!obj || !obj.ctx || !obj.e) return
598
- const wrapper = obj.w ? obj.w : null
599
- this.lastInFocus = document.activeElement
600
- const { ctx, e: content } = obj
601
-
602
- switch (ctx) {
603
- case 'ctxTranscript':
604
- this.openTranscript(content, wrapper)
605
- break
606
-
607
- case 'ctxBranching':
608
- if (this.compID === content) return this.closeSidebar('ctxBranching')
609
-
610
- this.openBranchContent(content)
611
- break
612
- }
613
- //delay animation
614
- this.rightSidebarVisible = true
615
- setTimeout(() => {
616
- const rightSidebarContent = this.getRightSidebar() // Emelent displayed in the sidebar-body
617
- if (!rightSidebarContent) return
618
- rightSidebarContent.scrollTop = 0
619
-
620
- const rSidebar = document.querySelector('#right-sidebar') // the sidebar
621
-
622
- rSidebar.setAttribute('tabindex', -1)
623
- this.resetFocus(rSidebar) //set focus on the sidebar
624
- }, 100)
625
- },
626
- /**
627
- * @description close the right sidebar component
628
- * @summary close sidebar according to context
629
- * @param {String} ctx - context in which side bar was opened and will now be closed
630
- *
631
- */
632
- closeSidebar(ctx, wrapper = null) {
633
- //delay animation
634
- this.rightSidebarVisible = false // this will allow to run the animation of sidebar closing 1rst
635
-
636
- this.resetFocus(this.lastInFocus)
637
- switch (ctx) {
638
- case 'ctxTranscript':
639
- this.closeTranscript(wrapper)
640
- break
641
-
642
- case 'ctxBranching':
643
- this.closeBranchContent()
644
- break
645
-
646
- default:
647
- this.branchingVisible = false
648
- this.transcriptVisible = false
649
- this.transcriptContent = null
650
- this.compID = null
651
- }
652
- },
653
- /**
654
- * @description to close a pop up (not currently used)
655
- * @param {Function} [cb]
656
- * @fires popup-close to AppBasePopup.vue
657
- */
658
- closePopup(cb) {
659
- this.popupIsOpen = false
660
- this.$bus.$emit('popup-close', cb)
661
- },
662
- /**
663
- * @description to open a pop up
664
- * @param {Object} data the content op the popup
665
- * @fires popup-open to AppBasePopup.vue
666
- */
667
- openPopup(data) {
668
- this.popupIsOpen = true
669
- this.$bus.$emit('popup-open', data) // Use to send message to popUp component
670
- },
671
-
672
- /**
673
- * @description to close a popover
674
- * @fires tooltip-close to AppCompToolTip.vue
675
- */
676
- closeToolTip() {
677
- this.$bus.$emit('tooltip-close')
678
- },
679
-
680
- /**
681
- * @description to open a popover
682
- * @param {Object} data the options of the popover
683
- * @fires tooltip-open to AppCompToolTip.vue
684
- */
685
- openToolTip(data) {
686
- this.$bus.$emit('tooltip-open', data) // Use to send message to tooltip component
687
- },
688
-
689
- /**
690
- * @description Manage opening of sidebar in transcript context
691
- * @summary set the value of the transcriptContent and transcripVisibility
692
- * @param {Object} c content to display in the sidebar
693
- */
694
-
695
- openTranscript(c, container) {
696
- if (!c) return (this.transcriptVisible = false)
697
- //Change transcript content
698
- if (this.transcriptContent !== c) this.transcriptContent = c
699
-
700
- if (this.transcriptContainer && this.transcriptContainer !== container) {
701
- /*console.log('transcriptContainer est différent de container')
702
- console.log({ ancien: this.transcriptContainer, nouveau: container })
703
- console.log('agrandir ancien container et réduire nouveau container')*/
704
- }
705
- this.transcriptContainer = container
706
- this.transcriptVisible = true
707
- },
708
- /**
709
- * @description Manage closing of sidebar in transcript context
710
- * @summary reset the value of the transcriptContent and transcripVisibility
711
- * @fires 'transcript-hidden' to AppCompPlaybar
712
- */
713
- closeTranscript(container = this.transcriptContainer) {
714
- setTimeout(() => {
715
- this.transcriptContent = null
716
- this.transcriptVisible = false
717
- }, 300)
718
- this.$bus.$emit('transcript-hidden')
719
- this.$bus.$emit('resize-media', 'lg', container)
720
- },
721
-
722
- /**
723
- * @description Handle the content of the branch page to display in the right sidebar.
724
- * @summary When call set the value of compID and the branching visibility Attach lister for scroll event in the sidebar
725
- * @param {Object} branchID - ID OF the Component That to retrieve
726
- * @fires 'branch-page-viewed' to $PageMixins when scroll in branch page reaches the bottom
727
- */
728
- openBranchContent(branchID) {
729
- this.branchingVisible = true
730
- this.compID = branchID //set compenent ID
731
-
732
- setTimeout(() => {
733
- const rightSidebar = this.getRightSidebar()
734
- //Should indicate that page is completed when the Rightsidebar content heigh is less then window height
735
- if (rightSidebar.scrollHeight <= window.innerHeight) {
736
- if (!this.getCurrentBranchPage) return
737
- let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
738
- this.getCurrentBranchPage.state = 'completed' // update the state of this branch
739
- userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
740
- this.$bus.$emit('branch-page-viewed')
741
- }
742
-
743
- rightSidebar.addEventListener('scroll', this.handleRightSidebarScroll)
744
- }, 300)
745
- },
746
-
747
- /**
748
- * @description Close the branch page in the right sidebar
749
- * * @summary When call remove listner for scroll event in the sidebar , reset compID and branch visibility
750
- * * @fires 'branching-hidden' to AppCompButtonProgress
751
- */
752
- closeBranchContent() {
753
- const rightSidebar = this.getRightSidebar()
754
-
755
- if (!rightSidebar) return
756
-
757
- rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
758
-
759
- setTimeout(() => {
760
- this.compID = null //reset the comp
761
- this.branchingVisible = false
762
- }, 300)
763
- this.$bus.$emit('branching-hidden')
764
- },
765
-
766
- /**
767
- * @description reset the values of state (currentTimeline,currentMedialement, currentPage, media duration, appStatus')in the store
768
- */
769
- async unload() {
770
- return new Promise((res) => {
771
- this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
772
- this.updateCurrentTimeline('')
773
- this.updateCurrentMediaElements([])
774
- this.updateCurrentPage({})
775
- this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
776
- res(this.getCompStatusTracker)
777
- })
778
- },
779
-
780
- /**
781
- * @description fetch a page from the store
782
- */
783
- async fetchPage() {
784
- this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
785
- const fetched = await new Promise((resolve) => {
786
- setTimeout(() => {
787
- resolve(this.getCurrentPage)
788
- this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
789
- }, 200)
790
- })
791
- return fetched
792
- },
793
- /**
794
- * @description get User data
795
- */
796
- async fetchUserData() {
797
- const fetched = await new Promise((resolve) => {
798
- // get user saved data after 200 milliseconds
799
- setTimeout(() => {
800
- resolve(this.getUserInteraction)
801
- }, 200)
802
- })
803
- return fetched
804
- },
805
-
806
- /**
807
- * @description format data from a page
808
- * @param {Object} page
809
- */
810
- formateData(page) {
811
- if (page && page.type === 'pg_media') {
812
- const {
813
- id,
814
- animation,
815
- type,
816
- mediaData: { mSources, mType, mSubtitle, mPoster, mTranscript },
817
- timeline,
818
- mElement
819
- } = page
820
- return {
821
- id,
822
- animation,
823
- type,
824
- mSources,
825
- mType,
826
- mSubtitle,
827
- timeline,
828
- mElement,
829
- mPoster,
830
- mTranscript
831
- }
832
- } else if (page && page.type === 'pg_animation') {
833
- const { id, animation, type, timeline } = page
834
- return {
835
- id,
836
- animation,
837
- type,
838
- timeline
839
- }
840
- } else return false
841
- },
842
- async initLesson() {
843
- await this.unload().then(() => {
844
- const packageType = this.getModuleInfo.packageType
845
- switch (packageType) {
846
- case 'scorm': {
847
- // launching the lesson (if the lesson is not already completed)
848
- const lessonStatus = this.$scorm.GetValue(
849
- 'cmi.core.lesson_status',
850
- true
851
- )
852
- if (lessonStatus === 'unknown') {
853
- this.$scorm.setValue('cmi.core.lesson_status', 'incomplete') // set the lesson status to in complete
854
- this.$scorm.Commit() // persist data
855
- }
856
-
857
- // check for the bookmark existance in LMS
858
- const bookmark = this.$scorm.GetValue(
859
- 'cmi.core.lesson_location',
860
- false
861
- )
862
-
863
- // if none stored in the LMS redirect to the 1st page
864
- if (
865
- !this.bookmarkActive ||
866
- bookmark === '' ||
867
- bookmark === undefined
868
- ) {
869
- //Redirect to current page or to menu if route is module
870
- this.$route.name && this.$route.name !== 'module'
871
- ? this.$router.push({ name: this.$route.name })
872
- : this.$router.push({ name: 'menu' })
873
- } else if (this.bookmarkActive && bookmark) {
874
- this.$router.push({ name: bookmark })
875
- }
876
- // set the current page
877
- this.fetchPage()
878
- this.$scorm.getScormAPI()
879
- break
880
- }
881
- case 'xapi':
882
- {
883
- //Check if there is a user connection
884
-
885
- if (
886
- this.getConnectionInfo &&
887
- this.getConnectionInfo.actor &&
888
- this.getConnectionInfo.remote &&
889
- this.getDataFromServer
890
- ) {
891
- const { lessonPosition } = this.getDataFromServer
892
- const bookmark = lessonPosition.length ? lessonPosition[0] : ''
893
- // if none stored in the LMS redirect to the 1st page
894
- if (
895
- !this.bookmarkActive ||
896
- bookmark === '' ||
897
- bookmark === undefined
898
- ) {
899
- //Redirect to current page or to menu if route is module
900
- this.$route.name && this.$route.name !== 'module'
901
- ? this.$router.push({ name: this.$route.name })
902
- : this.$router.push({ name: 'menu' })
903
- } else if (this.bookmarkActive && bookmark) {
904
- // go to last reached page
905
- this.$router.push({ name: bookmark })
906
- }
907
- } else {
908
- //Redirect to current page or to menu if route is module
909
-
910
- this.$route.name && this.$route.name !== 'module'
911
- ? this.$router.push({ name: this.$route.name })
912
- : this.$router.push({ name: 'menu' })
913
- }
914
- }
915
- break
916
- }
917
- })
918
- },
919
-
920
- /**
921
- * @description save interaction or Module state to scrom suspend_data
922
- * @param {String} arg [module|userInteraction| null]
923
- */
924
- saveToScorm(arg) {
925
- arg = arg || null
926
- let existingRecord // hold record record
927
- let toBeSaved // hold data to send to scorm
928
- if (this.$scorm.initialized) {
929
- if (this.$scorm.GetValue('cmi.suspend_data') !== '')
930
- existingRecord = JSON.parse(this.$scorm.GetValue('cmi.suspend_data')) // try convert the scorm record to JSON Obiect
931
- // create entry for user data if there is no record in Scorm
932
- if (!existingRecord) existingRecord = {}
933
- // There is a passed arg
934
- if (arg !== null) {
935
- // update only the user record
936
- if (arg === 'userInteraction') {
937
- // create new entry for user data if does not existe
938
- if (!existingRecord['userData']) existingRecord['userData'] = {}
939
- // update value of user data
940
- if (this.getUserInteraction)
941
- existingRecord.userData = this.getUserInteraction
942
- toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
943
- }
944
- // update only the module record
945
- else if (arg === 'module') {
946
- // create new entry for module data if does not existe
947
- if (!existingRecord['moduleData']) existingRecord['moduleData'] = {}
948
- // update value for module data
949
- toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
950
- }
951
- // no valid arg
952
- else return
953
- }
954
- // Update module & user record
955
- else {
956
- // create entry for module and user data if does not existe
957
- if (
958
- !existingRecord['userData'] &&
959
- !existingRecord['moduleData'] &&
960
- !existingRecord['routeHistory']
961
- ) {
962
- existingRecord['moduleData'] = {}
963
- existingRecord['userData'] = {}
964
- existingRecord['routeHistory'] = []
965
- }
966
- // update values for module and user data
967
- existingRecord.userData = this.getUserInteraction
968
- existingRecord.routeHistory = this.routeHistory
969
- // update value of user data
970
- existingRecord.moduleConfig = JSON.stringify({}) // update value of module data
971
- toBeSaved = JSON.stringify(existingRecord)
972
- }
973
- this.$scorm.SetValue('cmi.suspend_data', toBeSaved) // converte to serialized string and save to scorm
974
- this.$scorm.Commit() // persist data in LMS
975
- }
976
- },
977
- /**
978
- * @param {Object} to
979
- * @param {Object} from unused
980
- * @param {Function} next
981
- */
982
- updateContent(to, from, next) {
983
- if (this.getModuleInfo.packageType === 'scorm')
984
- this.saveToScorm('userInteraction') //save to scorm
985
-
986
- if (this.popupIsOpen) {
987
- //close popup and deactivate the popup animation
988
- this.closePopup({ animationOff: true })
989
- }
990
-
991
- if (this.rightSidebarVisible) this.closeSidebar()
992
-
993
- this.unload().then((res) => {
994
- if (res.length) {
995
- //======== Handle redirect to correct path when user enter incorrect path for existing route ====//
996
- let toName = null
997
- if (!to.name) {
998
- toName = to.fullPath.replace(/\//g, ' ').trim() //clean route name
999
-
1000
- //apply apply correct format for routes that concerne an activity
1001
- if (toName.includes('activite-')) {
1002
- toName = toName.split(' ')[0].replace('-', '_')
1003
- }
1004
- //Go to menu when introactive is false other wise will navigate to introduction
1005
- else if (
1006
- toName.includes('introduction') &&
1007
- !this.theIntroIsActivated
1008
- )
1009
- toName = 'menu'
1010
- }
1011
- //When User inter path to module
1012
- else if (to.name === 'module') {
1013
- toName = 'menu'
1014
- }
1015
- //Default redirection
1016
- else {
1017
- false
1018
- // next()
1019
- }
1020
- //===================== Handeling a page request from Store=========================== ====//
1021
- //get the page from store
1022
- this.fetchPage().then((res) => {
1023
- //Save current page has bookmark in scorm
1024
- if (
1025
- this.getModuleInfo.packageType === 'scorm' &&
1026
- this.$scorm.initialized &&
1027
- this.bookmarkActive
1028
- )
1029
- this.$scorm.setBookMark(this.$route.name)
1030
- })
1031
- }
1032
- })
1033
- },
1034
-
1035
- updateRouteHistory(from) {
1036
- //The route is not in the history: should be push at the end of the history
1037
- const targetIndex = this.routeData.findIndex((r) => {
1038
- if (r.type === 'pg_menu' && r.id === from.meta.id) return r
1039
- else if (
1040
- `${r.activity_ref}_${r.id}` ===
1041
- `${from.meta.activity_ref}_${from.meta.id}`
1042
- )
1043
- return r
1044
- })
1045
-
1046
- //Remove route from history if already in
1047
- if (targetIndex !== -1) this.routeData.splice(targetIndex, 1)
1048
-
1049
- // Add route route in history
1050
- this.routeData.push(from.meta)
1051
-
1052
- if (this.routeData.length >= 4) this.routeData.shift()
1053
- },
1054
-
1055
- /**
1056
- * @description Helper fonction to launch extra ressource from this activity. Extra ressource is another lesson.
1057
- * @param {Object} res - Data of the ressource to launch
1058
- * @param {String} res.url
1059
- * @param {String} res.id
1060
- */
1061
- launchResource(res) {
1062
- const wrapper = this.$xapi.XAPIWrapper
1063
- const baseDomain = window.location.origin
1064
-
1065
- let { endpoint, auth, registration } = wrapper.lrs
1066
- let { actor, remote } = this.getConnectionInfo
1067
-
1068
- if (!actor || !remote) return
1069
-
1070
- actor = encodeURIComponent(JSON.stringify(actor))
1071
- endpoint = encodeURIComponent(endpoint)
1072
- registration = encodeURIComponent(registration)
1073
- auth = encodeURIComponent(auth)
1074
-
1075
- const activity_id = encodeURIComponent(res.id)
1076
-
1077
- if (!res.url.includes(baseDomain) && remote)
1078
- res.url = `${baseDomain}/${res.url}`
1079
-
1080
- const newUrlToLaunch = `${res.url}?endpoint=${endpoint}&auth=${auth}&actor=${actor}&registration=${registration}&activity_id=${activity_id}`
1081
- this.routeChangeCounter = 0 //reset counter after saving
1082
- this.$bus.$emit('fire-exit-event', () => {
1083
- window.location.replace(newUrlToLaunch)
1084
- })
1085
- },
1086
-
1087
- /**
1088
- * @description start the tutorial for first time users
1089
- * @requires '../assets/data/onboardingMessages.json'
1090
- * @param {Object} [onboardingMessages] the object for the messages of the tutorial
1091
- */
1092
- startOnboarding(onboardingMessages) {
1093
- if (onboardingMessages) {
1094
- if (this.getOnboardingEnabled) {
1095
- //flags error for OnboardingMessages
1096
- this.validateOnboardingMessages(onboardingMessages)
1097
- try {
1098
- this.onboardingMessages = onboardingMessages
1099
- this.nextOnboarding('message_1')
1100
- } catch (e) {
1101
- //fetch default values for popups
1102
- this.onboardingMessages = import(
1103
- '../assets/data/onboardingMessages.json'
1104
- )
1105
- this.nextOnboarding('message_1')
1106
- }
1107
- }
1108
- } else {
1109
- if (this.getOnboardingEnabled) {
1110
- this.onboardingMessages = import(
1111
- '../assets/data/onboardingMessages.json'
1112
- )
1113
- this.nextOnboarding('message_1')
1114
- }
1115
- }
1116
- },
1117
-
1118
- /**
1119
- * @description start the tutorial for first time users
1120
- * @param {String} nextMessage name of the following messsage
1121
- */
1122
- nextOnboarding(nextMessage) {
1123
- let messages = this.onboardingMessages
1124
- //create message_X variable to put in cb_$confirm
1125
- let postNextMessage = 'message_' + (parseInt(nextMessage.slice(8)) + 1)
1126
-
1127
- if (messages[postNextMessage]) {
1128
- //add something to the cb_$confirm to nextOnboarding
1129
- messages[nextMessage].value.cb_$confirm = () => {
1130
- this.nextOnboarding(postNextMessage)
1131
- }
1132
- //to apply the target to tooltip before the mounted happens
1133
- if (messages[postNextMessage].type == 'tooltip') {
1134
- this.toolTipTarget = messages[postNextMessage].value.target
1135
- }
1136
- }
1137
- //check if the next message is a popup or other componant
1138
- if (messages[nextMessage].type == 'popup-avert') {
1139
- this.openPopup(messages[nextMessage])
1140
- } else if (messages[nextMessage].type == 'tooltip') {
1141
- this.openToolTip(messages[nextMessage].value)
1142
- }
1143
- },
1144
- /**
1145
- * @description- Skip directly to the main containt*
1146
- * main content is Node with page ID by default
1147
- * Main Content can be defined by front-end using prop "main" in Module.vue of project
1148
- * @fire {event} 'move-to-target' to AppBase
1149
- */
1150
-
1151
- skipToMain() {
1152
- const { main } = this.moduleConfig // check for main input
1153
-
1154
- let skipTo = main ? main : 'wrapper-content' // search for node element specified as main
1155
-
1156
- //fires event
1157
- this.$bus.$emit('move-to-target', skipTo, {
1158
- top: 100, // offset target top to 100 pixel
1159
- left: 0,
1160
- behavior: 'auto'
1161
- })
1162
- },
1163
-
1164
- /**
1165
- * @description validate the OnboardingMessages
1166
- * @param {Object} onboardingMessages
1167
- * @returns {Boolean} false if valid onboardingMessages || true if invalid onboardingMessages
1168
- */
1169
- validateOnboardingMessages(onboardingMessages) {
1170
- let err = false
1171
- let errorList = [] //array for errors dectected
1172
- if (onboardingMessages && typeof onboardingMessages == 'object') {
1173
- //continue
1174
- //check the list of attributes is message_X
1175
- let listMessage = Object.keys(onboardingMessages)
1176
- for (let index = 1; index <= listMessage.length; index++) {
1177
- const element = listMessage[index - 1]
1178
- if (element == 'message_' + index) {
1179
- //continu
1180
- if (
1181
- onboardingMessages[element] &&
1182
- typeof onboardingMessages[element] == 'object'
1183
- ) {
1184
- if (
1185
- onboardingMessages[element].type &&
1186
- typeof onboardingMessages[element].type == 'string'
1187
- ) {
1188
- //checks if is tooltip
1189
- if (onboardingMessages[element].type == 'tooltip') {
1190
- let tooltip = onboardingMessages[element]
1191
- if (tooltip.value && typeof tooltip.value == 'object') {
1192
- //check title
1193
- if (
1194
- !tooltip.value.title ||
1195
- typeof tooltip.value.title !== 'string'
1196
- ) {
1197
- //flags error
1198
- errorList.push('message_' + index + ' type value title')
1199
- console.warn(
1200
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1201
- index +
1202
- ' type value title is incorrect',
1203
- 'background: orange; color: white; display: block; margin:5px;'
1204
- )
1205
- }
1206
- //check content
1207
- if (
1208
- !tooltip.value.content ||
1209
- typeof tooltip.value.content !== 'string'
1210
- ) {
1211
- //flags error
1212
- errorList.push('message_' + index + ' type value content')
1213
- console.warn(
1214
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1215
- index +
1216
- ' type value content is incorrect',
1217
- 'background: orange; color: white; display: block; margin:5px;'
1218
- )
1219
- }
1220
- //check target
1221
- if (
1222
- tooltip.value.target &&
1223
- typeof tooltip.value.target == 'string'
1224
- ) {
1225
- //check if target is exist
1226
- let target = tooltip.value.target
1227
- //@todo
1228
- if (!document.getElementById(target)) {
1229
- //flags error
1230
- errorList.push(
1231
- 'message_' + index + ' type value target'
1232
- )
1233
- console.warn(
1234
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1235
- index +
1236
- ' type value target is not found in page',
1237
- 'background: orange; color: white; display: block; margin:5px;'
1238
- )
1239
- }
1240
- } else {
1241
- //flags error
1242
- errorList.push('message_' + index + ' type value target')
1243
- console.warn(
1244
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1245
- index +
1246
- ' type value target is incorrect',
1247
- 'background: orange; color: white; display: block; margin:5px;'
1248
- )
1249
- }
1250
- } else {
1251
- //flags error
1252
- errorList.push('message_' + index + ' type value')
1253
- console.warn(
1254
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1255
- index +
1256
- ' type value is incorrect',
1257
- 'background: orange; color: white; display: block; margin:5px;'
1258
- )
1259
- }
1260
- }
1261
- } else {
1262
- //flags error
1263
- errorList.push('message_' + index + ' type')
1264
- console.warn(
1265
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1266
- index +
1267
- ' type is missing',
1268
- 'background: orange; color: white; display: block; margin:5px;'
1269
- )
1270
- }
1271
- } else {
1272
- //flags error
1273
- errorList.push('message_' + index + ' typeof')
1274
- console.warn(
1275
- '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1276
- index +
1277
- ' is not an object',
1278
- 'background: orange; color: white; display: block; margin:5px;'
1279
- )
1280
- }
1281
- } else {
1282
- //flags error
1283
- errorList.push('onboardingMessages keys')
1284
- console.warn(
1285
- '%c WARNING!>>> appBaseModule: Your onboardingMessages keys does not follow "message_[number]" naming',
1286
- 'background: orange; color: white; display: block; margin:5px;'
1287
- )
1288
- }
1289
- }
1290
- } else {
1291
- //flags error
1292
- errorList.push('onboardingMessages')
1293
- console.warn(
1294
- '%c WARNING!>>> appBaseModule: Your onboardingMessages is not an Object',
1295
- 'background: orange; color: white; display: block; margin:5px;'
1296
- )
1297
- }
1298
- // all item have required field. There is no error
1299
- if (!errorList.length) err = false
1300
- //return list of error
1301
- else err = errorList
1302
-
1303
- return err
1304
- },
1305
-
1306
- /**
1307
- * @description Reset the focus the element
1308
- * @param {HTMLElement} e - element that will get focus
1309
- */
1310
- resetFocus(e) {
1311
- if (e) e.focus()
1312
- },
1313
-
1314
- handleRightSidebarScroll(event) {
1315
- let scrollHeight = null
1316
- let clientHeight = null
1317
- let scrollTop = null
1318
-
1319
- scrollHeight = event.target.scrollHeight
1320
- clientHeight = event.target.clientHeight
1321
- scrollTop = event.target.scrollTop
1322
-
1323
- // //Set scroll limit reached at 150px above the document height.
1324
- let scrollLimit = scrollHeight - 150
1325
- let fullyScrolled = Math.round(clientHeight + scrollTop)
1326
-
1327
- //consider page completed when scrolled value has reached or passed set limit
1328
- if (fullyScrolled >= scrollLimit) {
1329
- event.target.removeEventListener(
1330
- 'scroll',
1331
- this.handleRightSidebarScroll
1332
- )
1333
- let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
1334
-
1335
- this.getCurrentBranchPage.state = 'completed' // update the state of this branch
1336
- userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
1337
-
1338
- setTimeout(() => this.$bus.$emit('branch-page-viewed'), 20)
1339
- }
1340
- },
1341
- getRightSidebar() {
1342
- let rSidebar = null
1343
- rSidebar = document.getElementById('right-sidebar')
1344
- if (!rSidebar) return
1345
- const rSidebarContent = rSidebar.querySelector(`#right-sidebar-body`)
1346
- return rSidebarContent
1347
- },
1348
-
1349
- startTimeout() {
1350
- this.timeOut = setTimeout(() => {
1351
- this.cancelTimeout()
1352
- }, 3000)
1353
- },
1354
- cancelTimeout() {
1355
- if (this.timeOut) {
1356
- this.closeDelay = false
1357
- clearTimeout(this.timeOut)
1358
- }
1359
- },
1360
-
1361
- /**
1362
- * @description Method to handle to sanding of completion status to LRS
1363
- * determine wether to send a completion for an activity or the Lesson
1364
- * Completion is sent once
1365
- * @param {String} context - 'ACTIVITY | LESSON'
1366
- */
1367
-
1368
- sendCompletionStatus(context = null) {
1369
- if (
1370
- !this.getModuleInfo.packageType === 'xapi' ||
1371
- !this.getConnectionInfo ||
1372
- !this.getConnectionInfo.actor ||
1373
- !this.getConnectionInfo.remote ||
1374
- !this.getDataFromServer ||
1375
- !context
1376
- )
1377
- return
1378
-
1379
- let stmt = null
1380
-
1381
- switch (context) {
1382
- case 'ACTIVITY': {
1383
- let text
1384
- let completedSize = 0
1385
- Object.entries(this.getAllCompleted).forEach((value) => {
1386
- completedSize = completedSize + value[1].length
1387
- })
1388
- let recordInServerLength,
1389
- thisActivityProgressLength,
1390
- thisActivityLength
1391
-
1392
- const { userProgress } = this.getDataFromServer
1393
- const thisActivityServerState =
1394
- userProgress[this.$route.meta.activity_ref]
1395
-
1396
- if (thisActivityServerState)
1397
- recordInServerLength = Object.keys(thisActivityServerState).length
1398
-
1399
- thisActivityProgressLength = this.getAllCompleted[
1400
- this.$route.meta.activity_ref
1401
- ]
1402
- ? this.getAllCompleted[this.$route.meta.activity_ref].length
1403
- : 0
1404
-
1405
- //get The total length of the current activity
1406
- thisActivityLength = this.getAllActivities(
1407
- this.getCurrentPage.activityRef
1408
- ).pageSize
1409
-
1410
- if (thisActivityProgressLength !== thisActivityLength) return
1411
- if (thisActivityProgressLength == recordInServerLength) return
1412
-
1413
- //Defining the text to display for stmt description and definition
1414
- const id = this.getCurrentPage.activityRef
1415
- let aName = ''
1416
- switch (true) {
1417
- case id == 'A00':
1418
- aName = 'Introduction'
1419
- break
1420
- case id == 'A99':
1421
- aName = 'Conclusion'
1422
- break
1423
-
1424
- default: {
1425
- let d = id.replace('A', '').trim()
1426
- d = parseInt(d)
1427
- aName = `${this.$t('text.activity')} ${d}`
1428
- }
1429
- }
1430
-
1431
- switch (this.$i18n.locale) {
1432
- case 'fr':
1433
- if (this.getModuleInfo.courseID)
1434
- text = `${aName} de ${this.getModuleInfo.id} `
1435
- else text = `Le ${this.getModuleInfo.id}`
1436
- break
1437
- case 'en':
1438
- if (this.getModuleInfo.courseID)
1439
- text = `${aName} of ${this.getModuleInfo.id}`
1440
- else text = `The ${this.getModuleInfo.id}`
1441
- break
1442
- }
1443
-
1444
- // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1445
- // Note: User Settings are sent on a different URI and statement
1446
- const { isFistTime, userSettings, ...lessonsData } =
1447
- this.getUserInteraction
1448
-
1449
- stmt = {
1450
- id: this.getCurrentPage.activityRef,
1451
- verb: 'completed',
1452
- definition: text,
1453
- description: text,
1454
- extensions: [
1455
- {
1456
- id: 'user-data',
1457
- content: {
1458
- routeHistory: this.getRouteHistory,
1459
- ...lessonsData
1460
- }
1461
- }
1462
- ],
1463
- duration: this.activityDuration,
1464
- completion: true
1465
- }
1466
-
1467
- break
1468
- }
1469
-
1470
- case 'LESSON': {
1471
- //======================================================
1472
- let completedSize = 0
1473
- Object.entries(this.getAllCompleted).forEach((value) => {
1474
- completedSize = completedSize + value[1].length
1475
- })
1476
-
1477
- if (completedSize === this.getAllActivities().pageSize) {
1478
- let text
1479
- //Defining the text to display for stmt description and definition
1480
- switch (this.$i18n.locale) {
1481
- case 'fr':
1482
- if (this.getModuleInfo.courseID)
1483
- text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
1484
- else text = `Le ${this.getModuleInfo.id}`
1485
- break
1486
- case 'en':
1487
- if (this.getModuleInfo.courseID)
1488
- text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
1489
- else text = `The ${this.getModuleInfo.id}`
1490
- break
1491
- }
1492
-
1493
- // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1494
- // Note: User Settings are sent on a different URI and statement
1495
- const { isFistTime, userSettings, ...lessonsData } =
1496
- this.getUserInteraction
1497
-
1498
- stmt = {
1499
- verb: 'completed',
1500
- definition: text,
1501
- description: text,
1502
- extensions: [
1503
- {
1504
- id: 'user-data',
1505
- content: {
1506
- routeHistory: this.getRouteHistory,
1507
- ...lessonsData
1508
- }
1509
- }
1510
- ],
1511
- duration: this.lessonDuration,
1512
- completion: true
1513
- }
1514
- this.lessonCompletionStatus = true // set the status of this lesson as completed
1515
- let completedState = {
1516
- duration: this.lessonDuration,
1517
- completion: this.lessonCompletionStatus
1518
- }
1519
- this.updateDataFetchFromServer({
1520
- completedState
1521
- })
1522
- }
1523
-
1524
- break
1525
- }
1526
- }
1527
- if (!stmt) return
1528
-
1529
- this.$bus.$emit('send-xapi-statement', stmt)
1530
- },
1531
- /**
1532
- * @description Method handle start event of activity to LRS
1533
- * Check if the activity is already initiated in The LRS record to determine wether it a start or a resume
1534
- * @param {Object} a - Data of the activity
1535
- * @fires 'send-xapi-statement' to APPBASE
1536
- */
1537
-
1538
- sendStartStatement(a) {
1539
- if (!a || !this.getDataFromServer) return
1540
- const { id } = a
1541
- let aName = null
1542
-
1543
- switch (true) {
1544
- case id == 'A00':
1545
- aName = 'Introduction'
1546
- break
1547
- case id == 'A99':
1548
- aName = 'Conclusion'
1549
- break
1550
-
1551
- default: {
1552
- let d = id.replace('A', '').trim()
1553
- d = parseInt(d)
1554
- aName = `${this.$t('text.activity')} ${d}`
1555
- }
1556
- }
1557
-
1558
- let text =
1559
- this.$i18n.locale == 'fr'
1560
- ? `${aName} de ${this.getModuleInfo.id}`
1561
- : `${aName} of ${this.getModuleInfo.id}`
1562
-
1563
- /*
1564
- *Determine if activity as been initialized. Activity is initialized when it is in the serverRecords
1565
- */
1566
- const { userProgress } = this.getDataFromServer
1567
- const thisActivityServerState = userProgress[id] ? userProgress[id] : {}
1568
- const recordInServerLength = Object.keys(thisActivityServerState).length
1569
-
1570
- const stmt = {
1571
- id,
1572
- verb: recordInServerLength ? 'resumed' : 'initialized', //determine verb of the statement to send
1573
- definition: text,
1574
- description: text
1575
- }
1576
- setTimeout(() => this.$bus.$emit('send-xapi-statement', stmt), 500)
1577
- }
1578
- }
1579
- }
1580
- </script>
1581
- <style lang="scss">
1582
- .module {
1583
- width: 100%;
1584
- height: 100%;
1585
- min-height: 100vh;
1586
- position: relative;
1587
- display: flex;
1588
- flex-direction: row;
1589
- align-items: stretch;
1590
- }
1591
-
1592
- .skip-link {
1593
- position: absolute;
1594
- left: -999px;
1595
- top: auto;
1596
- width: 1px;
1597
- height: 1px;
1598
- overflow: hidden;
1599
- z-index: -999;
1600
-
1601
- &:focus,
1602
- &:active {
1603
- left: auto;
1604
- top: auto;
1605
- width: 30%;
1606
- height: auto;
1607
- overflow: auto;
1608
- margin: 10px 35%;
1609
- padding: 5px;
1610
- text-align: center;
1611
- font-size: 1.2em;
1612
- z-index: 999;
1613
- }
1614
- }
1615
- /********** FM **********/
1616
- #Loading {
1617
- width: 100%;
1618
- position: fixed;
1619
- z-index: 99999;
1620
- left: 0;
1621
- opacity: 0;
1622
- display: none;
1623
- transition: opacity 0.5s;
1624
- pointer-events: none;
1625
- }
1626
-
1627
- #right-sidebar {
1628
- z-index: 10;
1629
- display: flex;
1630
- flex-direction: column;
1631
- flex-wrap: nowrap;
1632
- justify-content: flex-start;
1633
- max-width: 780px;
1634
- width: 30%;
1635
- height: 100%;
1636
- overflow: hidden;
1637
- position: fixed;
1638
- right: 0;
1639
- top: 0;
1640
- background-color: #ffffff;
1641
- -webkit-box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1642
- box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1643
-
1644
- #right-sidebar-header {
1645
- display: flex;
1646
- flex-direction: column;
1647
- align-items: flex-end;
1648
- padding: 32px 20px 0 32px;
1649
-
1650
- .embranchement-close {
1651
- padding: 11px 13px;
1652
- }
1653
- }
1654
-
1655
- #right-sidebar-body {
1656
- max-height: 90%;
1657
- overflow-y: auto;
1658
- }
1659
- }
1660
-
1661
- .right-sidebar-transition-enter-active {
1662
- transition: transform 0.33s ease-out;
1663
- }
1664
-
1665
- .right-sidebar-transition-leave-active {
1666
- transition: transform 0.16s;
1667
- }
1668
-
1669
- .right-sidebar-transition-enter-from,
1670
- .right-sidebar-transition-leave-to {
1671
- transform: translateX(100%);
1672
- }
1673
- </style>
1
+ <!--
2
+ *@ Description: This component is used as main container to display application and containt to display
3
+ *@ What it does: The component fetch the data for the page to display on navigation.
4
+ *
5
+ *@Note :Must be used
6
+ -->
7
+
8
+ <template>
9
+ <div fluid class="module">
10
+ <div
11
+ id="page_info_section"
12
+ aria-labelledby="page_info"
13
+ aria-live="true"
14
+ ></div>
15
+ <a class="skip-link" href="" @click.prevent="skipToMain">
16
+ {{ $t('message.skip_content') }}
17
+ </a>
18
+
19
+ <nav
20
+ v-show="!isMenu"
21
+ id="navTool"
22
+ :key="$route.fullpath"
23
+ class="app-nav"
24
+ :class="{ show: closeDelay }"
25
+ >
26
+ <!------------------------ Nav for drMode ------------------>
27
+ <!-- <app-comp-table-of-content /> -->
28
+ <!------------------------ Nav for FullMode ---------------->
29
+
30
+ <app-comp-navigation
31
+ :app-status="appReady ? true : null"
32
+ :auto-navigate="theNavigationBetweenActivity"
33
+ />
34
+ </nav>
35
+ <base-module :m-data="$data">
36
+ <v-container
37
+ id="wrapper-content"
38
+ fluid
39
+ :class="{ active: moduleConfig.videoFull }"
40
+ class="scroll-bar"
41
+ >
42
+ <div class="box">
43
+ <router-view ref="main" :key="$route.fullPath" />
44
+ <!-- <router-view v-show="appReady" ref="main" :key="$route.fullPath" /> -->
45
+ </div>
46
+
47
+ <div id="primary_nav_wrapper"></div>
48
+ </v-container>
49
+ </base-module>
50
+
51
+ <!------------------POPUP QUICK -------------------------->
52
+ <app-comp-pop-up-next v-show="popupIsOpen">
53
+ <!-- <template #content></template> -->
54
+ </app-comp-pop-up-next>
55
+ <!------------------END USE POP UP-------------------------->
56
+
57
+ <!--------------RIGHT SIDEBAR (for display of extra contents)------------>
58
+ <Transition name="right-sidebar-transition" mode="out-in">
59
+ <div
60
+ v-if="rightSidebarVisible"
61
+ id="right-sidebar"
62
+ ref="right-sidebar"
63
+ :key="dynamicSidebarContent._id"
64
+ :class="{
65
+ 'v-media': dynamicSidebarContent._context === 'ctxTranscript'
66
+ // 'right-sidebar-show': rightSidebarVisible
67
+ }"
68
+ :aria-label="dynamicSidebarContent._label"
69
+ >
70
+ <div id="right-sidebar-header">
71
+ <button
72
+ class="btn-reserve-ico embranchement-close"
73
+ @click="
74
+ closeSidebar(
75
+ dynamicSidebarContent._context,
76
+ dynamicSidebarContent._container
77
+ ? dynamicSidebarContent._container
78
+ : null
79
+ )
80
+ "
81
+ >
82
+ <svg>
83
+ <use href="#close-square-icon" />
84
+ </svg>
85
+ <span class="sr-only">{{ $t('button.closePopUp') }}</span>
86
+ </button>
87
+ </div>
88
+
89
+ <div id="right-sidebar-body">
90
+ <component
91
+ :is="dynamicSidebarContent._component"
92
+ class="v-media"
93
+ v-bind="{ ...dynamicSidebarContent._comProps }"
94
+ />
95
+ <!-- </Transition> -->
96
+ </div>
97
+ <div id="right-sidebar-footer"></div>
98
+ </div>
99
+ </Transition>
100
+ <footer></footer>
101
+ <!-------------------------END RIGHT SIDEBAR------------------------------->
102
+ </div>
103
+ </template>
104
+ <script>
105
+ import { mapState, mapActions } from 'pinia'
106
+ import { useAppStore } from '../module/stores/appStore'
107
+ import BaseModule from './BaseModule.vue'
108
+ import { timerMixin } from '../mixins/timerMixin'
109
+ import AppCompTranscript from './AppCompTranscript.vue'
110
+ import { defineAsyncComponent } from 'vue'
111
+ //import { fileAssets } from '../shared/generalfuncs'
112
+ //import
113
+ export default {
114
+ components: {
115
+ BaseModule,
116
+ AppCompTranscript
117
+ },
118
+ mixins: [timerMixin],
119
+ props: {
120
+ moduleConfig: {
121
+ type: Object,
122
+ default: () => {
123
+ return {
124
+ allowNavigationToActivity: false, // set Previous/Next can allow navigation between activities. Set to false if do not want navigation between activities with Previous/Next
125
+ main: ''
126
+ }
127
+ }
128
+ }
129
+ },
130
+ data() {
131
+ return {
132
+ meta: {},
133
+ videoFull: null,
134
+ routeData: [],
135
+ changePage: false,
136
+ randKey: Math.floor(Math.random() * 10001),
137
+ popupIsOpen: false,
138
+ hidePlayBar: false, // Controle visibility of the play bar. set to true to hide play bar
139
+ stmt: null, // holder for xapi statememt,
140
+ moduleTimer: 0, //tracker for overall time spent the lesson
141
+ activityTimer: 0, // tracker for the time spent on activity,
142
+ routeChangeCounter: 0,
143
+ toolTipTarget: '', //for the tool tip,
144
+ onboardingMessages: {}, //for the onboarding @todo replace with default file
145
+ settingsSelected: {},
146
+ compID: null,
147
+ rightSidebarVisible: false,
148
+ lastInFocus: null,
149
+ closeDelay: false,
150
+ timeOut: null,
151
+ infocusTabIndex: null,
152
+ transcriptVisible: false,
153
+ transcriptContent: null,
154
+ transcriptContainer: null,
155
+ branchingVisible: false,
156
+ lessonCompletionStatus: false,
157
+ checkedDataFromServer: 0
158
+ }
159
+ },
160
+ computed: {
161
+ ...mapState(useAppStore, [
162
+ 'getCurrentPage',
163
+ 'getCurrentBranchPage',
164
+ 'getAppStatus',
165
+ 'getUserInteraction',
166
+ 'getModuleInfo',
167
+ 'getAllActivities',
168
+ 'getAllCompleted',
169
+ 'getConnectionInfo',
170
+ 'hasMediaElOrTimeline',
171
+ 'getMenuSettings',
172
+ 'getRouteHistory',
173
+ 'getOnboardingEnabled',
174
+ 'getApplicationSettings',
175
+ 'getDataFromServer',
176
+ 'getCompStatusTracker'
177
+ ]),
178
+ isMenu() {
179
+ return this.$route.name === 'menu'
180
+ },
181
+ appReady() {
182
+ return this.getAppStatus === 'ready' ? true : false
183
+ },
184
+
185
+ hasMedia() {
186
+ return typeof this.hasMediaElOrTimeline === 'object'
187
+ },
188
+ activityHasChanged() {
189
+ const rd = this.routeData.toReversed()
190
+
191
+ if (
192
+ rd.length <= 1 ||
193
+ rd[1].activity_ref !== this.$route.meta.activity_ref
194
+ )
195
+ return true
196
+ else return false
197
+ },
198
+
199
+ /**
200
+ * @description Set the id the module
201
+ */
202
+ theId() {
203
+ let id = 'mod_001'
204
+ if (this.moduleConfig.id) id = this.moduleConfig.id
205
+ return id
206
+ },
207
+ /**
208
+ * @description Set the title of the lesson
209
+ */
210
+ theTitle() {
211
+ let title = this.$t(`text.place_holder.for_lesson_title`)
212
+
213
+ if (this.getMenuSettings.lessonTitle)
214
+ title = this.getMenuSettings.lessonTitle
215
+ return title
216
+ },
217
+ /**
218
+ * @description Set desciption for the module
219
+ *
220
+ */
221
+ theDescription() {
222
+ let description = null
223
+
224
+ if (this.moduleConfig.description)
225
+ description = this.moduleConfig.description
226
+ return description
227
+ },
228
+
229
+ /**
230
+ * @description set Previous/Next can allow navigation between activities.
231
+ *
232
+ */
233
+ theNavigationBetweenActivity() {
234
+ let navBwteenActivity = false
235
+ if (this.moduleConfig.allowNavigationToActivity)
236
+ navBwteenActivity = this.moduleConfig.allowNavigationToActivity
237
+ return navBwteenActivity
238
+ },
239
+ /**
240
+ * @description Control use INTRODUCTION page in the Lesson.
241
+ * set to false if there is no introduction
242
+ */
243
+ theIntroIsActivated() {
244
+ let introActive = false
245
+ if (this.moduleConfig.introActive)
246
+ introActive = this.moduleConfig.introActive
247
+ return introActive
248
+ },
249
+ isMain() {
250
+ const { main } = this.moduleConfig
251
+ if (!main || main === ' ') return this.$route.meta.id
252
+
253
+ let mainEl = document.querySelector(`#${main}`)
254
+
255
+ if (!mainEl) return this.$route.meta.id
256
+
257
+ return mainEl
258
+ },
259
+
260
+ dynamicSidebarContent() {
261
+ if (!this.transcriptVisible && !this.branchingVisible) return null
262
+ let sidebarSettings = {}
263
+ let _label = null
264
+
265
+ if (this.transcriptVisible) {
266
+ _label =
267
+ this.$i18n.locale === 'fr'
268
+ ? 'Contenu de la transcription'
269
+ : 'Content of the transcript'
270
+
271
+ sidebarSettings = {
272
+ _component: AppCompTranscript,
273
+ _comProps: {
274
+ content: this.transcriptContent
275
+ },
276
+ _context: 'ctxTranscript',
277
+ _container: this.transcriptContainer,
278
+ _label,
279
+ _id: 'transcript'
280
+ }
281
+
282
+ //console.log(sidebarSettings)
283
+ } else if (this.branchingVisible) {
284
+ if (this.$route.meta.type !== 'branching' || !this.compID) return null
285
+
286
+ const componentName = this.compID
287
+ const { activityRef } = this.getCurrentPage //get activity id from current page
288
+ _label =
289
+ this.$i18n.locale === 'fr'
290
+ ? "contenu de l'embranchement"
291
+ : 'content of the branching'
292
+
293
+ sidebarSettings = {
294
+ _component: defineAsyncComponent(
295
+ () => import(`@/module/${activityRef}/${componentName}.vue`)
296
+ ),
297
+ _comProps: false,
298
+ _context: 'ctxBranching',
299
+ _label,
300
+ _id: componentName
301
+ }
302
+ }
303
+ return sidebarSettings
304
+ },
305
+
306
+ navigationHistory() {
307
+ return this.getRouteHistory
308
+ }
309
+ },
310
+ watch: {
311
+ navigationHistory: {
312
+ handler() {
313
+ this.routeData = this.navigationHistory
314
+ },
315
+ deep: true,
316
+ immediate: true
317
+ },
318
+ $route: {
319
+ handler() {
320
+ this.getFocusables().then((r) => {
321
+ //Pressing Tab or Shit + tab should make it possible to cycle focus through them
322
+ if (!r) return
323
+ r[0].focus()
324
+ this.infocusTabIndex = r[0]
325
+ })
326
+
327
+ //update the routeChangeCounter when navigation
328
+ if (this.routeChangeCounter < 3) this.routeChangeCounter += 1
329
+
330
+ /*
331
+ *Start Timer on New activities
332
+ */
333
+
334
+ const trackedRouteType = ['introduction', 'conclusion', 'normal']
335
+ if (trackedRouteType.includes(this.$route.meta.type)) {
336
+ if (this.$route.name.includes('.')) return
337
+
338
+ //Start the timer every time that there is a new activity
339
+ if (this.timerState === 'started') this.stopTimer('activity') // reset the activity timer
340
+ this.startTimer('activity')
341
+
342
+ //Send statement when activity as changed
343
+ if (this.activityHasChanged)
344
+ this.sendStartStatement({ id: this.$route.meta.activity_ref })
345
+ }
346
+ this.transcriptVisible = false
347
+ },
348
+ deep: true,
349
+ immediate: true
350
+ },
351
+
352
+ /**
353
+ * @description Defined in Store Watch all completed activities to send a lesson completion statement
354
+ */
355
+ getAllCompleted: {
356
+ handler() {
357
+ // Check once the server to set the completion status of the lesson
358
+ if (this.getDataFromServer && this.checkedDataFromServer < 1) {
359
+ const { completedState } = this.getDataFromServer
360
+
361
+ this.lessonCompletionStatus =
362
+ !completedState || !completedState.completion
363
+ ? false
364
+ : completedState.completion
365
+ this.checkedDataFromServer += 1
366
+ }
367
+
368
+ if (!this.lessonCompletionStatus) this.sendCompletionStatus('LESSON')
369
+ },
370
+ deep: true,
371
+ immediate: true
372
+ },
373
+ /**
374
+ * @description Defined in timerMixin Watch The epalsed time for autosaving the user reached position
375
+ */
376
+ elapsedTime: {
377
+ handler() {
378
+ if (this.timerState !== 'started') return
379
+ //send a statement every x time (second)
380
+ if (
381
+ this.timerState === 'started' &&
382
+ this.elapsedTime % 500 === 0 &&
383
+ this.getModuleInfo.packageType === 'xapi' &&
384
+ this.getConnectionInfo &&
385
+ this.getConnectionInfo.actor &&
386
+ this.getConnectionInfo.remote &&
387
+ this.getDataFromServer
388
+ ) {
389
+ const { lessonPosition } = this.getDataFromServer
390
+
391
+ const lastReached = lessonPosition.length ? lessonPosition[0] : ''
392
+
393
+ //only Send savepoint statement when 3 navigation happen and the route is different from last saved
394
+ if (
395
+ this.routeChangeCounter >= 3 &&
396
+ lastReached !== this.$route.name
397
+ ) {
398
+ let text
399
+ //Defining the text to display for stmt description and definition
400
+ switch (this.$i18n.locale) {
401
+ case 'fr':
402
+ if (this.getModuleInfo.courseID)
403
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
404
+ else text = `Le ${this.getModuleInfo.id}`
405
+ break
406
+ case 'en':
407
+ if (this.getModuleInfo.courseID)
408
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
409
+ else text = `The ${this.getModuleInfo.id}`
410
+ break
411
+ }
412
+
413
+ //Creating custom statment
414
+ const stmt = {
415
+ verb: 'progressed',
416
+ definition: text,
417
+ description: text,
418
+ extensions: [
419
+ {
420
+ id: 'ending-point',
421
+ content: (() =>
422
+ this.$route.name == 'menu'
423
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
424
+ ._namedRoute
425
+ : this.$route.name)()
426
+ }
427
+ ],
428
+ duration: this.lessonDuration
429
+ }
430
+
431
+ this.$bus.$emit('send-xapi-statement', stmt)
432
+ this.routeChangeCounter = 0 //reset counter after saving
433
+ }
434
+ }
435
+ },
436
+ deep: true
437
+ }
438
+ },
439
+ beforeUnmount() {
440
+ //Communication events
441
+ this.$bus.$off('close-sidebar', this.closeSidebar)
442
+ this.$bus.$off('open-sidebar', this.openSidebar)
443
+ this.$bus.$off('open-popup', this.openPopup)
444
+ this.$bus.$off('close-popup', this.closePopup)
445
+ this.$bus.$off('start-onboarding', this.startOnboarding)
446
+ this.$bus.$off('videoFullScreen', this.onVideoFullScreen)
447
+ this.$bus.$off('save-to-scorm', this.saveToScorm)
448
+ this.$bus.$off('launch-xapi-resource', this.launchResource)
449
+ this.$bus.$off('update-route-history', this.updateRouteHistory)
450
+ this.$bus.$off('update-content', this.updateContent)
451
+ this.$bus.$off('show-transcript', this.openTranscript)
452
+ this.$bus.$off('send-completion-event', this.sendCompletionStatus)
453
+ this.$bus.$off('send-starting-event', this.sendStartStatement)
454
+ //nav mouseleave event
455
+ let nav = document.getElementById('navTool')
456
+ if (nav) {
457
+ nav.removeEventListener('mouseleave', this.onNavMouseleave)
458
+ }
459
+ //sidebar scroll event
460
+ const rightSidebar = this.getRightSidebar()
461
+ if (rightSidebar) {
462
+ rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
463
+ }
464
+ //keayboard listener
465
+ document.removeEventListener('keydown', this.handleKeyboardControls)
466
+ },
467
+ created() {
468
+ let lessonLabel, lessonNumber, lessonTitle, titleString
469
+ lessonLabel = this.$t('text.lesson')
470
+ lessonNumber = this.moduleConfig.id.replace('module_', '')
471
+ lessonTitle = this.theTitle
472
+ titleString = lessonLabel + ' ' + lessonNumber + ' : ' + lessonTitle
473
+
474
+ //Remove prefix for introduction or conclusion, according to isIntroConclu setting in Module.vue
475
+ if (typeof this.moduleConfig.isIntroConclu !== 'undefined') {
476
+ if (this.moduleConfig.isIntroConclu) {
477
+ titleString = lessonTitle
478
+ }
479
+ }
480
+ document.title = titleString
481
+
482
+ // Tell the store the state of intro and conclusion
483
+ this.updateIntroStatus(this.theIntroIsActivated)
484
+ //Communication events
485
+ this.$bus.$on('open-popup', this.openPopup)
486
+
487
+ this.$bus.$on('close-popup', this.closePopup)
488
+
489
+ this.$bus.$on('start-onboarding', this.startOnboarding)
490
+
491
+ this.$bus.$on('open-sidebar', this.openSidebar)
492
+
493
+ this.$bus.$on('close-sidebar', this.closeSidebar)
494
+
495
+ this.$bus.$on('videoFullScreen', this.onVideoFullScreen)
496
+
497
+ this.$bus.$on('save-to-scorm', this.saveToScorm)
498
+
499
+ this.$bus.$on('launch-xapi-resource', this.launchResource)
500
+
501
+ this.$bus.$on('update-route-history', this.updateRouteHistory)
502
+ this.$bus.$on('update-content', this.updateContent)
503
+ this.$bus.$on('show-transcript', this.openTranscript)
504
+ this.$bus.$on('send-completion-event', this.sendCompletionStatus)
505
+ this.$bus.$on('send-starting-event', this.sendStartStatement)
506
+
507
+ this.initLesson()
508
+
509
+ if (this.navigationHistory.length != 0) {
510
+ this.routeData = this.navigationHistory
511
+ }
512
+ setTimeout(() => {
513
+ this.settingsSelected = { ...this.getApplicationSettings }
514
+ }, 800)
515
+ },
516
+ mounted() {
517
+ //A11Y: Bring back the focus on the body element after each navigation
518
+ document.body.setAttribute('tabindex', -1) //needed to use .focus()
519
+ this.$router.afterEach(() => {
520
+ document.body.focus()
521
+ })
522
+
523
+ let nav = document.getElementById('navTool')
524
+
525
+ if (nav) nav.addEventListener('mouseleave', this.onNavMouseleave)
526
+
527
+ document.addEventListener('keydown', this.handleKeyboardControls)
528
+ },
529
+ methods: {
530
+ ...mapActions(useAppStore, [
531
+ 'updateIntroStatus',
532
+ 'updateCurrentTimeline',
533
+ 'updateCurrentMediaElements',
534
+ 'updateCurrentPage',
535
+ 'updateDataFetchFromServer'
536
+ ]),
537
+ onNavMouseleave() {
538
+ let widgetOpen = document.getElementsByClassName('open')
539
+
540
+ if (widgetOpen.length == 0) {
541
+ this.cancelTimeout()
542
+ this.closeDelay = true
543
+ this.startTimeout()
544
+ } else {
545
+ this.closeDelay = true
546
+ }
547
+ },
548
+ onVideoFullScreen(value) {
549
+ this.videoFull = value
550
+ },
551
+ /* Get All Element related to the media return a list of all DOM element*/
552
+ async getFocusables() {
553
+ return new Promise(function (resolve, reject) {
554
+ setTimeout(function () {
555
+ const getAllFocusables = () => {
556
+ const listItems = document.querySelectorAll('.v-media')
557
+ if (!listItems || !listItems.length) return null
558
+ return Array.from(listItems)
559
+ }
560
+ resolve(getAllFocusables())
561
+ }, 200)
562
+ })
563
+ },
564
+ /**
565
+ * @description Handle Keyboard controls
566
+ * @summary
567
+ * @param {Object} evt - event that fired the action
568
+ *
569
+ */
570
+ async handleKeyboardControls(evt) {
571
+ let { code } = evt
572
+
573
+ if (code === 'Escape' && this.rightSidebarVisible)
574
+ return (this.rightSidebarVisible = false)
575
+
576
+ //==========================================
577
+ /*const videoRange = await this.getFocusables()
578
+ if (videoRange && videoRange.indexOf(document.activeElement) === -1)
579
+ return
580
+ this.$bus.$emit('play-media', code)*/
581
+ },
582
+
583
+ /**
584
+ * @description open the right sidebar component and show and set it content
585
+ * @summary opens sidebar according to context
586
+ * @param {Object} obj {ctx , e} - context and content to display in the sidebar
587
+ *
588
+ */
589
+ openSidebar(obj) {
590
+ if (!obj || !obj.ctx || !obj.e) return
591
+ const wrapper = obj.w ? obj.w : null
592
+ this.lastInFocus = document.activeElement
593
+ const { ctx, e: content } = obj
594
+
595
+ switch (ctx) {
596
+ case 'ctxTranscript':
597
+ this.openTranscript(content, wrapper)
598
+ break
599
+
600
+ case 'ctxBranching':
601
+ if (this.compID === content) return this.closeSidebar('ctxBranching')
602
+
603
+ this.openBranchContent(content)
604
+ break
605
+ }
606
+ //delay animation
607
+ this.rightSidebarVisible = true
608
+ setTimeout(() => {
609
+ const rightSidebarContent = this.getRightSidebar() // Emelent displayed in the sidebar-body
610
+ if (!rightSidebarContent) return
611
+ rightSidebarContent.scrollTop = 0
612
+
613
+ const rSidebar = document.querySelector('#right-sidebar') // the sidebar
614
+
615
+ rSidebar.setAttribute('tabindex', -1)
616
+ this.resetFocus(rSidebar) //set focus on the sidebar
617
+ }, 100)
618
+ },
619
+ /**
620
+ * @description close the right sidebar component
621
+ * @summary close sidebar according to context
622
+ * @param {String} ctx - context in which side bar was opened and will now be closed
623
+ *
624
+ */
625
+ closeSidebar(ctx, wrapper = null) {
626
+ //delay animation
627
+ this.rightSidebarVisible = false // this will allow to run the animation of sidebar closing 1rst
628
+
629
+ this.resetFocus(this.lastInFocus)
630
+ switch (ctx) {
631
+ case 'ctxTranscript':
632
+ this.closeTranscript(wrapper)
633
+ break
634
+
635
+ case 'ctxBranching':
636
+ this.closeBranchContent()
637
+ break
638
+
639
+ default:
640
+ this.branchingVisible = false
641
+ this.transcriptVisible = false
642
+ this.transcriptContent = null
643
+ this.compID = null
644
+ }
645
+ },
646
+ /**
647
+ * @description to close a pop up (not currently used)
648
+ * @param {Function} [cb]
649
+ * @fires popup-close to AppBasePopup.vue
650
+ */
651
+ closePopup(cb) {
652
+ this.popupIsOpen = false
653
+ this.$bus.$emit('popup-close', cb)
654
+ },
655
+ /**
656
+ * @description to open a pop up
657
+ * @param {Object} data the content op the popup
658
+ * @fires popup-open to AppBasePopup.vue
659
+ */
660
+ openPopup(data) {
661
+ this.popupIsOpen = true
662
+ this.$bus.$emit('popup-open', data) // Use to send message to popUp component
663
+ },
664
+
665
+ /**
666
+ * @description to close a popover
667
+ * @fires tooltip-close to AppCompToolTip.vue
668
+ */
669
+ closeToolTip() {
670
+ this.$bus.$emit('tooltip-close')
671
+ },
672
+
673
+ /**
674
+ * @description to open a popover
675
+ * @param {Object} data the options of the popover
676
+ * @fires tooltip-open to AppCompToolTip.vue
677
+ */
678
+ openToolTip(data) {
679
+ this.$bus.$emit('tooltip-open', data) // Use to send message to tooltip component
680
+ },
681
+
682
+ /**
683
+ * @description Manage opening of sidebar in transcript context
684
+ * @summary set the value of the transcriptContent and transcripVisibility
685
+ * @param {Object} c content to display in the sidebar
686
+ */
687
+
688
+ openTranscript(c, container) {
689
+ if (!c) return (this.transcriptVisible = false)
690
+ //Change transcript content
691
+ if (this.transcriptContent !== c) this.transcriptContent = c
692
+
693
+ if (this.transcriptContainer && this.transcriptContainer !== container) {
694
+ /*console.log('transcriptContainer est différent de container')
695
+ console.log({ ancien: this.transcriptContainer, nouveau: container })
696
+ console.log('agrandir ancien container et réduire nouveau container')*/
697
+ }
698
+ this.transcriptContainer = container
699
+ this.transcriptVisible = true
700
+ },
701
+ /**
702
+ * @description Manage closing of sidebar in transcript context
703
+ * @summary reset the value of the transcriptContent and transcripVisibility
704
+ * @fires 'transcript-hidden' to AppCompPlaybar
705
+ */
706
+ closeTranscript(container = this.transcriptContainer) {
707
+ setTimeout(() => {
708
+ this.transcriptContent = null
709
+ this.transcriptVisible = false
710
+ }, 300)
711
+ this.$bus.$emit('transcript-hidden')
712
+ this.$bus.$emit('resize-media', 'lg', container)
713
+ },
714
+
715
+ /**
716
+ * @description Handle the content of the branch page to display in the right sidebar.
717
+ * @summary When call set the value of compID and the branching visibility Attach lister for scroll event in the sidebar
718
+ * @param {Object} branchID - ID OF the Component That to retrieve
719
+ * @fires 'branch-page-viewed' to $PageMixins when scroll in branch page reaches the bottom
720
+ */
721
+ openBranchContent(branchID) {
722
+ this.branchingVisible = true
723
+ this.compID = branchID //set compenent ID
724
+
725
+ setTimeout(() => {
726
+ const rightSidebar = this.getRightSidebar()
727
+ //Should indicate that page is completed when the Rightsidebar content heigh is less then window height
728
+ if (rightSidebar.scrollHeight <= window.innerHeight) {
729
+ if (!this.getCurrentBranchPage) return
730
+ let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
731
+ this.getCurrentBranchPage.state = 'completed' // update the state of this branch
732
+ userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
733
+ this.$bus.$emit('branch-page-viewed')
734
+ }
735
+
736
+ rightSidebar.addEventListener('scroll', this.handleRightSidebarScroll)
737
+ }, 300)
738
+ },
739
+
740
+ /**
741
+ * @description Close the branch page in the right sidebar
742
+ * * @summary When call remove listner for scroll event in the sidebar , reset compID and branch visibility
743
+ * * @fires 'branching-hidden' to AppCompButtonProgress
744
+ */
745
+ closeBranchContent() {
746
+ const rightSidebar = this.getRightSidebar()
747
+
748
+ if (!rightSidebar) return
749
+
750
+ rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
751
+
752
+ setTimeout(() => {
753
+ this.compID = null //reset the comp
754
+ this.branchingVisible = false
755
+ }, 300)
756
+ this.$bus.$emit('branching-hidden')
757
+ },
758
+
759
+ /**
760
+ * @description reset the values of state (currentTimeline,currentMedialement, currentPage, media duration, appStatus')in the store
761
+ */
762
+ async unload() {
763
+ return new Promise((res) => {
764
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
765
+ this.updateCurrentTimeline('')
766
+ this.updateCurrentMediaElements([])
767
+ this.updateCurrentPage({})
768
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
769
+ res(this.getCompStatusTracker)
770
+ })
771
+ },
772
+
773
+ /**
774
+ * @description fetch a page from the store
775
+ */
776
+ async fetchPage() {
777
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
778
+ const fetched = await new Promise((resolve) => {
779
+ setTimeout(() => {
780
+ resolve(this.getCurrentPage)
781
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
782
+ }, 200)
783
+ })
784
+ return fetched
785
+ },
786
+ /**
787
+ * @description get User data
788
+ */
789
+ async fetchUserData() {
790
+ const fetched = await new Promise((resolve) => {
791
+ // get user saved data after 200 milliseconds
792
+ setTimeout(() => {
793
+ resolve(this.getUserInteraction)
794
+ }, 200)
795
+ })
796
+ return fetched
797
+ },
798
+
799
+ /**
800
+ * @description format data from a page
801
+ * @param {Object} page
802
+ */
803
+ formateData(page) {
804
+ if (page && page.type === 'pg_media') {
805
+ const {
806
+ id,
807
+ animation,
808
+ type,
809
+ mediaData: { mSources, mType, mSubtitle, mPoster, mTranscript },
810
+ timeline,
811
+ mElement
812
+ } = page
813
+ return {
814
+ id,
815
+ animation,
816
+ type,
817
+ mSources,
818
+ mType,
819
+ mSubtitle,
820
+ timeline,
821
+ mElement,
822
+ mPoster,
823
+ mTranscript
824
+ }
825
+ } else if (page && page.type === 'pg_animation') {
826
+ const { id, animation, type, timeline } = page
827
+ return {
828
+ id,
829
+ animation,
830
+ type,
831
+ timeline
832
+ }
833
+ } else return false
834
+ },
835
+ async initLesson() {
836
+ await this.unload().then(() => {
837
+ const packageType = this.getModuleInfo.packageType
838
+ switch (packageType) {
839
+ case 'scorm': {
840
+ // launching the lesson (if the lesson is not already completed)
841
+ const lessonStatus = this.$scorm.GetValue(
842
+ 'cmi.core.lesson_status',
843
+ true
844
+ )
845
+ if (lessonStatus === 'unknown') {
846
+ this.$scorm.setValue('cmi.core.lesson_status', 'incomplete') // set the lesson status to in complete
847
+ this.$scorm.Commit() // persist data
848
+ }
849
+
850
+ // check for the bookmark existance in LMS
851
+ const bookmark = this.$scorm.GetValue(
852
+ 'cmi.core.lesson_location',
853
+ false
854
+ )
855
+
856
+ // if none stored in the LMS redirect to the 1st page
857
+ if (bookmark === '' || bookmark === undefined) {
858
+ //Redirect to current page or to menu if route is module
859
+ this.$route.name && this.$route.name !== 'module'
860
+ ? this.$router.push({ name: this.$route.name })
861
+ : this.$router.push({ name: 'menu' })
862
+ } else if (bookmark) {
863
+ this.$router.push({ name: bookmark })
864
+ }
865
+ // set the current page
866
+ this.fetchPage()
867
+ this.$scorm.getScormAPI()
868
+ break
869
+ }
870
+ case 'xapi':
871
+ {
872
+ this.$route.name && this.$route.name !== 'module'
873
+ ? this.$router.push({ name: this.$route.name })
874
+ : this.$router.push({ name: 'menu' })
875
+
876
+ this.fetchPage()
877
+ // }
878
+ }
879
+ break
880
+ }
881
+ })
882
+ },
883
+
884
+ /**
885
+ * @description save interaction or Module state to scrom suspend_data
886
+ * @param {String} arg [module|userInteraction| null]
887
+ */
888
+ saveToScorm(arg) {
889
+ arg = arg || null
890
+ let existingRecord // hold record record
891
+ let toBeSaved // hold data to send to scorm
892
+ if (this.$scorm.initialized) {
893
+ if (this.$scorm.GetValue('cmi.suspend_data') !== '')
894
+ existingRecord = JSON.parse(this.$scorm.GetValue('cmi.suspend_data')) // try convert the scorm record to JSON Obiect
895
+ // create entry for user data if there is no record in Scorm
896
+ if (!existingRecord) existingRecord = {}
897
+ // There is a passed arg
898
+ if (arg !== null) {
899
+ // update only the user record
900
+ if (arg === 'userInteraction') {
901
+ // create new entry for user data if does not existe
902
+ if (!existingRecord['userData']) existingRecord['userData'] = {}
903
+ // update value of user data
904
+ if (this.getUserInteraction)
905
+ existingRecord.userData = this.getUserInteraction
906
+ toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
907
+ }
908
+ // update only the module record
909
+ else if (arg === 'module') {
910
+ // create new entry for module data if does not existe
911
+ if (!existingRecord['moduleData']) existingRecord['moduleData'] = {}
912
+ // update value for module data
913
+ toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
914
+ }
915
+ // no valid arg
916
+ else return
917
+ }
918
+ // Update module & user record
919
+ else {
920
+ // create entry for module and user data if does not existe
921
+ if (
922
+ !existingRecord['userData'] &&
923
+ !existingRecord['moduleData'] &&
924
+ !existingRecord['routeHistory']
925
+ ) {
926
+ existingRecord['moduleData'] = {}
927
+ existingRecord['userData'] = {}
928
+ existingRecord['routeHistory'] = []
929
+ }
930
+ // update values for module and user data
931
+ existingRecord.userData = this.getUserInteraction
932
+ existingRecord.routeHistory = this.routeData
933
+ // update value of user data
934
+ existingRecord.moduleConfig = JSON.stringify({}) // update value of module data
935
+ toBeSaved = JSON.stringify(existingRecord)
936
+ }
937
+ this.$scorm.SetValue('cmi.suspend_data', toBeSaved) // converte to serialized string and save to scorm
938
+ this.$scorm.Commit() // persist data in LMS
939
+ }
940
+ },
941
+ /**
942
+ * @param {Object} to
943
+ * @param {Object} from unused
944
+ * @param {Function} next
945
+ */
946
+ updateContent(to, from, next) {
947
+ if (this.getModuleInfo.packageType === 'scorm')
948
+ this.saveToScorm('userInteraction') //save to scorm
949
+
950
+ if (this.popupIsOpen) {
951
+ //close popup and deactivate the popup animation
952
+ this.closePopup({ animationOff: true })
953
+ }
954
+
955
+ if (this.rightSidebarVisible) this.closeSidebar()
956
+
957
+ this.unload().then((res) => {
958
+ if (res.length) {
959
+ //======== Handle redirect to correct path when user enter incorrect path for existing route ====//
960
+ let toName = null
961
+ if (!to.name) {
962
+ toName = to.fullPath.replace(/\//g, ' ').trim() //clean route name
963
+
964
+ //apply apply correct format for routes that concerne an activity
965
+ if (toName.includes('activite-')) {
966
+ toName = toName.split(' ')[0].replace('-', '_')
967
+ }
968
+ //Go to menu when introactive is false other wise will navigate to introduction
969
+ else if (
970
+ toName.includes('introduction') &&
971
+ !this.theIntroIsActivated
972
+ )
973
+ toName = 'menu'
974
+ }
975
+ //When User inter path to module
976
+ else if (to.name === 'module') {
977
+ toName = 'menu'
978
+ }
979
+ //Default redirection
980
+ else {
981
+ false
982
+ // next()
983
+ }
984
+ //===================== Handeling a page request from Store=========================== ====//
985
+ //get the page from store
986
+ this.fetchPage().then((res) => {
987
+ //Save current page has bookmark in scorm
988
+ if (
989
+ this.getModuleInfo.packageType === 'scorm' &&
990
+ this.$scorm.initialized
991
+ )
992
+ this.$scorm.setBookMark(this.$route.name)
993
+ })
994
+ }
995
+ })
996
+ },
997
+
998
+ updateRouteHistory(from) {
999
+ //The route is not in the history: should be push at the end of the history
1000
+ const targetIndex = this.routeData.findIndex((r) => {
1001
+ if (r.type === 'pg_menu' && r.id === from.meta.id) return r
1002
+ else if (
1003
+ `${r.activity_ref}_${r.id}` ===
1004
+ `${from.meta.activity_ref}_${from.meta.id}`
1005
+ )
1006
+ return r
1007
+ })
1008
+
1009
+ //Remove route from history if already in
1010
+ if (targetIndex !== -1) this.routeData.splice(targetIndex, 1)
1011
+
1012
+ // Add route route in history
1013
+ this.routeData.push(from.meta)
1014
+
1015
+ if (this.routeData.length >= 4) this.routeData.shift()
1016
+ },
1017
+
1018
+ /**
1019
+ * @description Helper fonction to launch extra ressource from this activity. Extra ressource is another lesson.
1020
+ * @param {Object} res - Data of the ressource to launch
1021
+ * @param {String} res.url
1022
+ * @param {String} res.id
1023
+ */
1024
+ launchResource(res) {
1025
+ const wrapper = this.$xapi.XAPIWrapper
1026
+ const baseDomain = window.location.origin
1027
+
1028
+ let { endpoint, auth, registration } = wrapper.lrs
1029
+ let { actor, remote } = this.getConnectionInfo
1030
+
1031
+ if (!actor || !remote) return
1032
+
1033
+ actor = encodeURIComponent(JSON.stringify(actor))
1034
+ endpoint = encodeURIComponent(endpoint)
1035
+ registration = encodeURIComponent(registration)
1036
+ auth = encodeURIComponent(auth)
1037
+
1038
+ const activity_id = encodeURIComponent(res.id)
1039
+
1040
+ if (!res.url.includes(baseDomain) && remote)
1041
+ res.url = `${baseDomain}/${res.url}`
1042
+
1043
+ const newUrlToLaunch = `${res.url}?endpoint=${endpoint}&auth=${auth}&actor=${actor}&registration=${registration}&activity_id=${activity_id}`
1044
+ this.routeChangeCounter = 0 //reset counter after saving
1045
+ this.$bus.$emit('fire-exit-event', () => {
1046
+ window.location.replace(newUrlToLaunch)
1047
+ })
1048
+ },
1049
+
1050
+ /**
1051
+ * @description start the tutorial for first time users
1052
+ * @requires '../assets/data/onboardingMessages.json'
1053
+ * @param {Object} [onboardingMessages] the object for the messages of the tutorial
1054
+ */
1055
+ startOnboarding(onboardingMessages) {
1056
+ if (onboardingMessages) {
1057
+ if (this.getOnboardingEnabled) {
1058
+ //flags error for OnboardingMessages
1059
+ this.validateOnboardingMessages(onboardingMessages)
1060
+ try {
1061
+ this.onboardingMessages = onboardingMessages
1062
+ this.nextOnboarding('message_1')
1063
+ } catch (e) {
1064
+ //fetch default values for popups
1065
+ this.onboardingMessages = import(
1066
+ '../assets/data/onboardingMessages.json'
1067
+ )
1068
+ this.nextOnboarding('message_1')
1069
+ }
1070
+ }
1071
+ } else {
1072
+ if (this.getOnboardingEnabled) {
1073
+ this.onboardingMessages = import(
1074
+ '../assets/data/onboardingMessages.json'
1075
+ )
1076
+ this.nextOnboarding('message_1')
1077
+ }
1078
+ }
1079
+ },
1080
+
1081
+ /**
1082
+ * @description start the tutorial for first time users
1083
+ * @param {String} nextMessage name of the following messsage
1084
+ */
1085
+ nextOnboarding(nextMessage) {
1086
+ let messages = this.onboardingMessages
1087
+ //create message_X variable to put in cb_$confirm
1088
+ let postNextMessage = 'message_' + (parseInt(nextMessage.slice(8)) + 1)
1089
+
1090
+ if (messages[postNextMessage]) {
1091
+ //add something to the cb_$confirm to nextOnboarding
1092
+ messages[nextMessage].value.cb_$confirm = () => {
1093
+ this.nextOnboarding(postNextMessage)
1094
+ }
1095
+ //to apply the target to tooltip before the mounted happens
1096
+ if (messages[postNextMessage].type == 'tooltip') {
1097
+ this.toolTipTarget = messages[postNextMessage].value.target
1098
+ }
1099
+ }
1100
+ //check if the next message is a popup or other componant
1101
+ if (messages[nextMessage].type == 'popup-avert') {
1102
+ this.openPopup(messages[nextMessage])
1103
+ } else if (messages[nextMessage].type == 'tooltip') {
1104
+ this.openToolTip(messages[nextMessage].value)
1105
+ }
1106
+ },
1107
+ /**
1108
+ * @description- Skip directly to the main containt*
1109
+ * main content is Node with page ID by default
1110
+ * Main Content can be defined by front-end using prop "main" in Module.vue of project
1111
+ * @fire {event} 'move-to-target' to AppBase
1112
+ */
1113
+
1114
+ skipToMain() {
1115
+ const { main } = this.moduleConfig // check for main input
1116
+
1117
+ let skipTo = main ? main : 'wrapper-content' // search for node element specified as main
1118
+
1119
+ //fires event
1120
+ this.$bus.$emit('move-to-target', skipTo, {
1121
+ top: 100, // offset target top to 100 pixel
1122
+ left: 0,
1123
+ behavior: 'auto'
1124
+ })
1125
+ },
1126
+
1127
+ /**
1128
+ * @description validate the OnboardingMessages
1129
+ * @param {Object} onboardingMessages
1130
+ * @returns {Boolean} false if valid onboardingMessages || true if invalid onboardingMessages
1131
+ */
1132
+ validateOnboardingMessages(onboardingMessages) {
1133
+ let err = false
1134
+ let errorList = [] //array for errors dectected
1135
+ if (onboardingMessages && typeof onboardingMessages == 'object') {
1136
+ //continue
1137
+ //check the list of attributes is message_X
1138
+ let listMessage = Object.keys(onboardingMessages)
1139
+ for (let index = 1; index <= listMessage.length; index++) {
1140
+ const element = listMessage[index - 1]
1141
+ if (element == 'message_' + index) {
1142
+ //continu
1143
+ if (
1144
+ onboardingMessages[element] &&
1145
+ typeof onboardingMessages[element] == 'object'
1146
+ ) {
1147
+ if (
1148
+ onboardingMessages[element].type &&
1149
+ typeof onboardingMessages[element].type == 'string'
1150
+ ) {
1151
+ //checks if is tooltip
1152
+ if (onboardingMessages[element].type == 'tooltip') {
1153
+ let tooltip = onboardingMessages[element]
1154
+ if (tooltip.value && typeof tooltip.value == 'object') {
1155
+ //check title
1156
+ if (
1157
+ !tooltip.value.title ||
1158
+ typeof tooltip.value.title !== 'string'
1159
+ ) {
1160
+ //flags error
1161
+ errorList.push('message_' + index + ' type value title')
1162
+ console.warn(
1163
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1164
+ index +
1165
+ ' type value title is incorrect',
1166
+ 'background: orange; color: white; display: block; margin:5px;'
1167
+ )
1168
+ }
1169
+ //check content
1170
+ if (
1171
+ !tooltip.value.content ||
1172
+ typeof tooltip.value.content !== 'string'
1173
+ ) {
1174
+ //flags error
1175
+ errorList.push('message_' + index + ' type value content')
1176
+ console.warn(
1177
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1178
+ index +
1179
+ ' type value content is incorrect',
1180
+ 'background: orange; color: white; display: block; margin:5px;'
1181
+ )
1182
+ }
1183
+ //check target
1184
+ if (
1185
+ tooltip.value.target &&
1186
+ typeof tooltip.value.target == 'string'
1187
+ ) {
1188
+ //check if target is exist
1189
+ let target = tooltip.value.target
1190
+ //@todo
1191
+ if (!document.getElementById(target)) {
1192
+ //flags error
1193
+ errorList.push(
1194
+ 'message_' + index + ' type value target'
1195
+ )
1196
+ console.warn(
1197
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1198
+ index +
1199
+ ' type value target is not found in page',
1200
+ 'background: orange; color: white; display: block; margin:5px;'
1201
+ )
1202
+ }
1203
+ } else {
1204
+ //flags error
1205
+ errorList.push('message_' + index + ' type value target')
1206
+ console.warn(
1207
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1208
+ index +
1209
+ ' type value target is incorrect',
1210
+ 'background: orange; color: white; display: block; margin:5px;'
1211
+ )
1212
+ }
1213
+ } else {
1214
+ //flags error
1215
+ errorList.push('message_' + index + ' type value')
1216
+ console.warn(
1217
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1218
+ index +
1219
+ ' type value is incorrect',
1220
+ 'background: orange; color: white; display: block; margin:5px;'
1221
+ )
1222
+ }
1223
+ }
1224
+ } else {
1225
+ //flags error
1226
+ errorList.push('message_' + index + ' type')
1227
+ console.warn(
1228
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1229
+ index +
1230
+ ' type is missing',
1231
+ 'background: orange; color: white; display: block; margin:5px;'
1232
+ )
1233
+ }
1234
+ } else {
1235
+ //flags error
1236
+ errorList.push('message_' + index + ' typeof')
1237
+ console.warn(
1238
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1239
+ index +
1240
+ ' is not an object',
1241
+ 'background: orange; color: white; display: block; margin:5px;'
1242
+ )
1243
+ }
1244
+ } else {
1245
+ //flags error
1246
+ errorList.push('onboardingMessages keys')
1247
+ console.warn(
1248
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages keys does not follow "message_[number]" naming',
1249
+ 'background: orange; color: white; display: block; margin:5px;'
1250
+ )
1251
+ }
1252
+ }
1253
+ } else {
1254
+ //flags error
1255
+ errorList.push('onboardingMessages')
1256
+ console.warn(
1257
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages is not an Object',
1258
+ 'background: orange; color: white; display: block; margin:5px;'
1259
+ )
1260
+ }
1261
+ // all item have required field. There is no error
1262
+ if (!errorList.length) err = false
1263
+ //return list of error
1264
+ else err = errorList
1265
+
1266
+ return err
1267
+ },
1268
+
1269
+ /**
1270
+ * @description Reset the focus the element
1271
+ * @param {HTMLElement} e - element that will get focus
1272
+ */
1273
+ resetFocus(e) {
1274
+ if (e) e.focus()
1275
+ },
1276
+
1277
+ handleRightSidebarScroll(event) {
1278
+ let scrollHeight = null
1279
+ let clientHeight = null
1280
+ let scrollTop = null
1281
+
1282
+ scrollHeight = event.target.scrollHeight
1283
+ clientHeight = event.target.clientHeight
1284
+ scrollTop = event.target.scrollTop
1285
+
1286
+ // //Set scroll limit reached at 150px above the document height.
1287
+ let scrollLimit = scrollHeight - 150
1288
+ let fullyScrolled = Math.round(clientHeight + scrollTop)
1289
+
1290
+ //consider page completed when scrolled value has reached or passed set limit
1291
+ if (fullyScrolled >= scrollLimit) {
1292
+ event.target.removeEventListener(
1293
+ 'scroll',
1294
+ this.handleRightSidebarScroll
1295
+ )
1296
+ let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
1297
+
1298
+ this.getCurrentBranchPage.state = 'completed' // update the state of this branch
1299
+ userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
1300
+
1301
+ setTimeout(() => this.$bus.$emit('branch-page-viewed'), 20)
1302
+ }
1303
+ },
1304
+ getRightSidebar() {
1305
+ let rSidebar = null
1306
+ rSidebar = document.getElementById('right-sidebar')
1307
+ if (!rSidebar) return
1308
+ const rSidebarContent = rSidebar.querySelector(`#right-sidebar-body`)
1309
+ return rSidebarContent
1310
+ },
1311
+
1312
+ startTimeout() {
1313
+ this.timeOut = setTimeout(() => {
1314
+ this.cancelTimeout()
1315
+ }, 3000)
1316
+ },
1317
+ cancelTimeout() {
1318
+ if (this.timeOut) {
1319
+ this.closeDelay = false
1320
+ clearTimeout(this.timeOut)
1321
+ }
1322
+ },
1323
+
1324
+ /**
1325
+ * @description Method to handle to sanding of completion status to LRS
1326
+ * determine wether to send a completion for an activity or the Lesson
1327
+ * Completion is sent once
1328
+ * @param {String} context - 'ACTIVITY | LESSON'
1329
+ */
1330
+
1331
+ sendCompletionStatus(context = null) {
1332
+ if (
1333
+ !this.getModuleInfo.packageType === 'xapi' ||
1334
+ !this.getConnectionInfo ||
1335
+ !this.getConnectionInfo.actor ||
1336
+ !this.getConnectionInfo.remote ||
1337
+ !this.getDataFromServer ||
1338
+ !context
1339
+ )
1340
+ return
1341
+
1342
+ let stmt = null
1343
+
1344
+ switch (context) {
1345
+ case 'ACTIVITY': {
1346
+ let text
1347
+ let completedSize = 0
1348
+ Object.entries(this.getAllCompleted).forEach((value) => {
1349
+ completedSize = completedSize + value[1].length
1350
+ })
1351
+ let recordInServerLength,
1352
+ thisActivityProgressLength,
1353
+ thisActivityLength
1354
+
1355
+ const { userProgress } = this.getDataFromServer
1356
+ const thisActivityServerState =
1357
+ userProgress[this.$route.meta.activity_ref]
1358
+
1359
+ if (thisActivityServerState)
1360
+ recordInServerLength = Object.keys(thisActivityServerState).length
1361
+
1362
+ thisActivityProgressLength = this.getAllCompleted[
1363
+ this.$route.meta.activity_ref
1364
+ ]
1365
+ ? this.getAllCompleted[this.$route.meta.activity_ref].length
1366
+ : 0
1367
+
1368
+ //get The total length of the current activity
1369
+ thisActivityLength = this.getAllActivities(
1370
+ this.getCurrentPage.activityRef
1371
+ ).pageSize
1372
+
1373
+ if (thisActivityProgressLength !== thisActivityLength) return
1374
+ if (thisActivityProgressLength == recordInServerLength) return
1375
+
1376
+ //Defining the text to display for stmt description and definition
1377
+ const id = this.getCurrentPage.activityRef
1378
+ let aName = ''
1379
+ switch (true) {
1380
+ case id == 'A00':
1381
+ aName = 'Introduction'
1382
+ break
1383
+ case id == 'A99':
1384
+ aName = 'Conclusion'
1385
+ break
1386
+
1387
+ default: {
1388
+ let d = id.replace('A', '').trim()
1389
+ d = parseInt(d)
1390
+ aName = `${this.$t('text.activity')} ${d}`
1391
+ }
1392
+ }
1393
+
1394
+ switch (this.$i18n.locale) {
1395
+ case 'fr':
1396
+ if (this.getModuleInfo.courseID)
1397
+ text = `${aName} de ${this.getModuleInfo.id} `
1398
+ else text = `Le ${this.getModuleInfo.id}`
1399
+ break
1400
+ case 'en':
1401
+ if (this.getModuleInfo.courseID)
1402
+ text = `${aName} of ${this.getModuleInfo.id}`
1403
+ else text = `The ${this.getModuleInfo.id}`
1404
+ break
1405
+ }
1406
+
1407
+ // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1408
+ // Note: User Settings are sent on a different URI and statement
1409
+ const { isFistTime, userSettings, ...lessonsData } =
1410
+ this.getUserInteraction
1411
+
1412
+ stmt = {
1413
+ id: this.getCurrentPage.activityRef,
1414
+ verb: 'completed',
1415
+ definition: text,
1416
+ description: text,
1417
+ extensions: [
1418
+ {
1419
+ id: 'user-data',
1420
+ content: {
1421
+ routeHistory: this.getRouteHistory,
1422
+ ...lessonsData
1423
+ }
1424
+ }
1425
+ ],
1426
+ duration: this.activityDuration,
1427
+ completion: true
1428
+ }
1429
+
1430
+ break
1431
+ }
1432
+
1433
+ case 'LESSON': {
1434
+ //======================================================
1435
+ let completedSize = 0
1436
+ Object.entries(this.getAllCompleted).forEach((value) => {
1437
+ completedSize = completedSize + value[1].length
1438
+ })
1439
+
1440
+ if (completedSize === this.getAllActivities().pageSize) {
1441
+ let text
1442
+ //Defining the text to display for stmt description and definition
1443
+ switch (this.$i18n.locale) {
1444
+ case 'fr':
1445
+ if (this.getModuleInfo.courseID)
1446
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
1447
+ else text = `Le ${this.getModuleInfo.id}`
1448
+ break
1449
+ case 'en':
1450
+ if (this.getModuleInfo.courseID)
1451
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
1452
+ else text = `The ${this.getModuleInfo.id}`
1453
+ break
1454
+ }
1455
+
1456
+ // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1457
+ // Note: User Settings are sent on a different URI and statement
1458
+ const { isFistTime, userSettings, ...lessonsData } =
1459
+ this.getUserInteraction
1460
+
1461
+ stmt = {
1462
+ verb: 'completed',
1463
+ definition: text,
1464
+ description: text,
1465
+ extensions: [
1466
+ {
1467
+ id: 'user-data',
1468
+ content: {
1469
+ routeHistory: this.getRouteHistory,
1470
+ ...lessonsData
1471
+ }
1472
+ }
1473
+ ],
1474
+ duration: this.lessonDuration,
1475
+ completion: true
1476
+ }
1477
+ this.lessonCompletionStatus = true // set the status of this lesson as completed
1478
+ let completedState = {
1479
+ duration: this.lessonDuration,
1480
+ completion: this.lessonCompletionStatus
1481
+ }
1482
+ this.updateDataFetchFromServer({
1483
+ completedState
1484
+ })
1485
+ }
1486
+
1487
+ break
1488
+ }
1489
+ }
1490
+ if (!stmt) return
1491
+
1492
+ this.$bus.$emit('send-xapi-statement', stmt)
1493
+ },
1494
+ /**
1495
+ * @description Method handle start event of activity to LRS
1496
+ * Check if the activity is already initiated in The LRS record to determine wether it a start or a resume
1497
+ * @param {Object} a - Data of the activity
1498
+ * @fires 'send-xapi-statement' to APPBASE
1499
+ */
1500
+
1501
+ sendStartStatement(a) {
1502
+ if (!a || !this.getDataFromServer) return
1503
+ const { id } = a
1504
+ let aName = null
1505
+
1506
+ switch (true) {
1507
+ case id == 'A00':
1508
+ aName = 'Introduction'
1509
+ break
1510
+ case id == 'A99':
1511
+ aName = 'Conclusion'
1512
+ break
1513
+
1514
+ default: {
1515
+ let d = id.replace('A', '').trim()
1516
+ d = parseInt(d)
1517
+ aName = `${this.$t('text.activity')} ${d}`
1518
+ }
1519
+ }
1520
+
1521
+ let text =
1522
+ this.$i18n.locale == 'fr'
1523
+ ? `${aName} de ${this.getModuleInfo.id}`
1524
+ : `${aName} of ${this.getModuleInfo.id}`
1525
+
1526
+ /*
1527
+ *Determine if activity as been initialized. Activity is initialized when it is in the serverRecords
1528
+ */
1529
+ const { userProgress } = this.getDataFromServer
1530
+ const thisActivityServerState = userProgress[id] ? userProgress[id] : {}
1531
+ const recordInServerLength = Object.keys(thisActivityServerState).length
1532
+
1533
+ const stmt = {
1534
+ id,
1535
+ verb: recordInServerLength ? 'resumed' : 'initialized', //determine verb of the statement to send
1536
+ definition: text,
1537
+ description: text
1538
+ }
1539
+ setTimeout(() => this.$bus.$emit('send-xapi-statement', stmt), 500)
1540
+ }
1541
+ }
1542
+ }
1543
+ </script>
1544
+ <style lang="scss">
1545
+ .module {
1546
+ width: 100%;
1547
+ height: 100%;
1548
+ min-height: 100vh;
1549
+ position: relative;
1550
+ display: flex;
1551
+ flex-direction: row;
1552
+ align-items: stretch;
1553
+ }
1554
+
1555
+ .skip-link {
1556
+ position: absolute;
1557
+ left: -999px;
1558
+ top: auto;
1559
+ width: 1px;
1560
+ height: 1px;
1561
+ overflow: hidden;
1562
+ z-index: -999;
1563
+
1564
+ &:focus,
1565
+ &:active {
1566
+ left: auto;
1567
+ top: auto;
1568
+ width: 30%;
1569
+ height: auto;
1570
+ overflow: auto;
1571
+ margin: 10px 35%;
1572
+ padding: 5px;
1573
+ text-align: center;
1574
+ font-size: 1.2em;
1575
+ z-index: 999;
1576
+ }
1577
+ }
1578
+ /********** FM **********/
1579
+ #Loading {
1580
+ width: 100%;
1581
+ position: fixed;
1582
+ z-index: 99999;
1583
+ left: 0;
1584
+ opacity: 0;
1585
+ display: none;
1586
+ transition: opacity 0.5s;
1587
+ pointer-events: none;
1588
+ }
1589
+
1590
+ #right-sidebar {
1591
+ z-index: 10;
1592
+ display: flex;
1593
+ flex-direction: column;
1594
+ flex-wrap: nowrap;
1595
+ justify-content: flex-start;
1596
+ max-width: 780px;
1597
+ width: 30%;
1598
+ height: 100%;
1599
+ overflow: hidden;
1600
+ position: fixed;
1601
+ right: 0;
1602
+ top: 0;
1603
+ background-color: #ffffff;
1604
+ -webkit-box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1605
+ box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1606
+
1607
+ #right-sidebar-header {
1608
+ display: flex;
1609
+ flex-direction: column;
1610
+ align-items: flex-end;
1611
+ padding: 32px 20px 0 32px;
1612
+
1613
+ .embranchement-close {
1614
+ padding: 11px 13px;
1615
+ }
1616
+ }
1617
+
1618
+ #right-sidebar-body {
1619
+ max-height: 90%;
1620
+ overflow-y: auto;
1621
+ }
1622
+ }
1623
+
1624
+ .right-sidebar-transition-enter-active {
1625
+ transition: transform 0.33s ease-out;
1626
+ }
1627
+
1628
+ .right-sidebar-transition-leave-active {
1629
+ transition: transform 0.16s;
1630
+ }
1631
+
1632
+ .right-sidebar-transition-enter-from,
1633
+ .right-sidebar-transition-leave-to {
1634
+ transform: translateX(100%);
1635
+ }
1636
+ </style>