fcad-core-dragon 2.1.1 → 2.1.2

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