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

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 (90) hide show
  1. package/.editorconfig +33 -33
  2. package/.eslintignore +29 -29
  3. package/.eslintrc.cjs +81 -81
  4. package/CHANGELOG +13 -0
  5. package/README.md +71 -71
  6. package/bk.scss +117 -117
  7. package/package.json +8 -8
  8. package/src/$locales/en.json +145 -143
  9. package/src/$locales/fr.json +107 -105
  10. package/src/assets/data/onboardingMessages.json +47 -47
  11. package/src/components/AppBase.vue +1150 -1054
  12. package/src/components/AppBaseButton.test.js +22 -0
  13. package/src/components/AppBaseButton.vue +93 -87
  14. package/src/components/AppBaseErrorDisplay.vue +438 -438
  15. package/src/components/AppBaseFlipCard.vue +84 -84
  16. package/src/components/AppBaseModule.vue +1657 -1673
  17. package/src/components/AppBasePage.vue +742 -779
  18. package/src/components/AppBasePopover.vue +41 -41
  19. package/src/components/AppCompAudio.vue +265 -234
  20. package/src/components/AppCompBranchButtons.vue +556 -552
  21. package/src/components/AppCompButtonProgress.vue +121 -126
  22. package/src/components/AppCompCarousel.vue +328 -298
  23. package/src/components/AppCompInputCheckBoxNext.vue +200 -195
  24. package/src/components/AppCompInputDropdownNext.vue +201 -159
  25. package/src/components/AppCompInputRadioNext.vue +152 -152
  26. package/src/components/AppCompInputTextNext.vue +125 -106
  27. package/src/components/AppCompInputTextTableNext.vue +142 -141
  28. package/src/components/AppCompInputTextToFillDropdownNext.vue +238 -230
  29. package/src/components/AppCompInputTextToFillNext.vue +171 -171
  30. package/src/components/AppCompJauge.vue +74 -74
  31. package/src/components/AppCompMenu.vue +25 -10
  32. package/src/components/AppCompMenuItem.vue +228 -228
  33. package/src/components/AppCompNavigation.vue +972 -960
  34. package/src/components/AppCompNoteCall.vue +159 -133
  35. package/src/components/AppCompNoteCredit.vue +490 -292
  36. package/src/components/AppCompPlayBar.vue +1217 -1218
  37. package/src/components/AppCompPlayBarNext.vue +2060 -2052
  38. package/src/components/AppCompPlayBarProgress.vue +82 -82
  39. package/src/components/AppCompPopUpNext.vue +500 -503
  40. package/src/components/AppCompQuizNext.vue +2908 -2904
  41. package/src/components/AppCompQuizRecall.vue +298 -276
  42. package/src/components/AppCompSVGNext.vue +347 -347
  43. package/src/components/AppCompSettingsMenu.vue +172 -172
  44. package/src/components/AppCompTableOfContent.vue +386 -387
  45. package/src/components/AppCompTranscript.vue +24 -24
  46. package/src/components/AppCompVideoPlayer.vue +368 -368
  47. package/src/components/BaseModule.vue +55 -72
  48. package/src/components/tests__/AppBaseButton.spec.js +53 -0
  49. package/src/composables/useQuiz.js +206 -206
  50. package/src/externalComps/ModuleView.vue +22 -22
  51. package/src/externalComps/SummaryView.vue +91 -91
  52. package/src/main.js +272 -272
  53. package/src/mixins/$mediaMixins.js +819 -819
  54. package/src/mixins/timerMixin.js +155 -155
  55. package/src/module/stores/appStore.js +954 -893
  56. package/src/module/xapi/ADL.js +380 -376
  57. package/src/module/xapi/Crypto/Hasher.js +241 -241
  58. package/src/module/xapi/Crypto/WordArray.js +278 -278
  59. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  60. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  61. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  62. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  63. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  64. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  65. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  66. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  67. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  68. package/src/module/xapi/Statement/agent.js +55 -55
  69. package/src/module/xapi/Statement/index.js +259 -259
  70. package/src/module/xapi/Statement/statement.js +253 -253
  71. package/src/module/xapi/launch.js +157 -157
  72. package/src/module/xapi/utils.js +167 -167
  73. package/src/module/xapi/verbs.js +294 -294
  74. package/src/module/xapi/wrapper.js +1963 -1963
  75. package/src/module/xapi/xapiStatement.js +444 -444
  76. package/src/plugins/bus.js +8 -8
  77. package/src/plugins/gsap.js +14 -14
  78. package/src/plugins/helper.js +355 -308
  79. package/src/plugins/i18n.js +44 -44
  80. package/src/plugins/idb.js +227 -219
  81. package/src/plugins/save.js +37 -37
  82. package/src/plugins/scorm.js +287 -287
  83. package/src/plugins/xapi.js +11 -11
  84. package/src/public/index.html +33 -33
  85. package/src/router/index.js +48 -43
  86. package/src/router/routes.js +312 -312
  87. package/src/shared/generalfuncs.js +210 -210
  88. package/src/shared/validators.js +926 -1069
  89. package/vitest.config.js +19 -0
  90. package/vite.config.js +0 -27
@@ -1,1054 +1,1150 @@
1
- <!--
2
- @ Description: This is a root component to create the App
3
- @ What it does:
4
- - validate the data for the App configuration
5
- - Send the xapi statement
6
- - Manage the fetching and setting of data from serveur
7
- - Example of use: <app-base :app-config="$data"></app-base>
8
- @ Must be used.
9
- -->
10
- <template>
11
- <div id="App-base" fluid :class="{ iPad: resizeiPad }">
12
- <template v-if="error.length">
13
- <v-row>
14
- <v-col>
15
- <app-base-error-display
16
- :errors-list="error"
17
- error-type="appConfig"
18
- error-title="Configuration de l'application"
19
- />
20
- </v-col>
21
- </v-row>
22
- </template>
23
- <template v-else>
24
- <transition name="bounce" mode="in-out">
25
- <div
26
- v-if="showBuildInfo && !buildInfoClicked"
27
- id="build-info"
28
- @click="buildInfoClicked = true"
29
- >
30
- <span>{{ getModuleInfo.courseID.toUpperCase() }}</span>
31
- <span>FCAD {{ $helper.getFcadVersion() }}</span>
32
- <span>{{ $helper.getBuildTime() }}</span>
33
- </div>
34
- </transition>
35
-
36
- <router-view class="box" />
37
-
38
- <app-icons-next :extra-icons="userExtraIcons" />
39
- </template>
40
-
41
- <v-overlay
42
- id="overlay_loading"
43
- :model-value="!appReady"
44
- class="align-center justify-center"
45
- scrim="white"
46
- blur="3px"
47
- opacity="0.8"
48
- >
49
- <div class="text-center grp-spinners">
50
- <v-progress-circular
51
- :size="50"
52
- :width="3"
53
- color="#003552"
54
- bg-color="#d3effe"
55
- indeterminate
56
- ></v-progress-circular>
57
-
58
- <span class="sr-only">
59
- {{ $t('message.loading_state_msg') }}
60
- </span>
61
- </div>
62
- </v-overlay>
63
- </div>
64
- </template>
65
- <script>
66
- import { mapState, mapActions } from 'pinia'
67
- import { useAppStore } from '../module/stores/appStore.js'
68
- import { timerMixin } from '../mixins/timerMixin'
69
- import { validateAppContent } from '../shared/validators'
70
- import AppBaseErrorDisplay from './AppBaseErrorDisplay.vue'
71
- import mobileDetect from 'mobile-detect'
72
- export default {
73
- components: { AppBaseErrorDisplay },
74
- mixins: [timerMixin],
75
- props: {
76
- appConfig: {
77
- type: Object,
78
- required: true,
79
- validator: (value) => {
80
- if (import.meta.env.DEV) return validateAppContent(value).length === 0
81
- }
82
- }
83
- },
84
- setup() {
85
- const store = useAppStore()
86
- return { store }
87
- },
88
-
89
- data() {
90
- return {
91
- randKey: null,
92
- isLandScape: null,
93
- initialDeviceOrentation: null,
94
- appIsFullScreen: false,
95
- resizeiPad: false,
96
- buildInfoClicked: false,
97
- error: []
98
- }
99
- },
100
- computed: {
101
- ...mapState(useAppStore, [
102
- 'getCurrentBrowser',
103
- 'getIsMobile',
104
- 'getDeviceType',
105
- 'getModuleInfo',
106
- 'getAppStatus',
107
- 'getUserInteraction',
108
- 'getConnectionInfo',
109
- 'getDataFromServer',
110
- 'getAllActivities',
111
- 'getMediaPlaybarValues',
112
- 'getAppConfigs',
113
- 'getErrorMenu'
114
- ]),
115
- getwidth() {
116
- return window.innerWidth
117
- },
118
- getheight() {
119
- return window.innerHeight
120
- },
121
-
122
- showBuildInfo() {
123
- return (
124
- import.meta.env.PROD &&
125
- window.location.host === 'projets.cegepadistance.ca'
126
- )
127
- },
128
- appReady() {
129
- let readyState = this.getAppStatus === 'ready' ? true : false
130
-
131
- return readyState
132
- },
133
- displayLang() {
134
- let lang = false
135
- const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
136
-
137
- if (this.getModuleInfo.packageType === 'xapi') {
138
- lang = displayList.find((l) => l.includes(this.$i18n.locale))
139
- }
140
- return lang
141
- },
142
- userExtraIcons() {
143
- const icons = this.$helper.getSettingsFromStore('extra_icons')
144
- ? this.$helper.getSettingsFromStore('extra_icons')
145
- : null
146
- return icons
147
- }
148
- },
149
- watch: {
150
- getConnectionInfo: {
151
- deep: true,
152
- //in development environment (localhost), don't wait for the axios call
153
- immediate: import.meta.env.DEV,
154
- handler() {
155
- /**
156
- * Attach listener to detect when user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile,
157
- * switches from the browser to a different app To save user data to LRS/LMS.
158
- * As of 06/2021 MDN Web Doc advise preferring the 'visibilitychange' event over 'unload/beforeunload'
159
- * event to send data over a serveur.
160
- * Ref:https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
161
- * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
162
- */
163
-
164
- if (!this.getConnectionInfo) return
165
- if (this.getIsMobile) {
166
- document.addEventListener(
167
- 'visibilitychange',
168
- () => {
169
- if (document.visibilityState === 'hidden') {
170
- this.executeCloseEventTriggered()
171
- }
172
- },
173
- false
174
- )
175
- }
176
- // initialize scorm Commuinication with LMS || xAPI LRS
177
- if (this.getModuleInfo.packageType === 'scorm') this.$scorm.Initialize()
178
- else if (
179
- this.getModuleInfo.packageType === 'xapi' &&
180
- this.getConnectionInfo &&
181
- this.getConnectionInfo.actor &&
182
- this.getConnectionInfo.remote
183
- ) {
184
- const {
185
- auth,
186
- endpoint,
187
- registration = this.$xapi.ruuid()
188
- } = this.getConnectionInfo
189
-
190
- const config = {
191
- auth,
192
- endpoint,
193
- registration,
194
- activity_platform: `SIPI_organizationId`
195
- }
196
-
197
- this.$xapi._configLRS(config) // configure and launch LRS
198
-
199
- setTimeout(() => {
200
- this.fetchDataFromServer().then(() => {
201
- setTimeout(() => {
202
- this.setProgress()
203
- }, 200)
204
- })
205
- }, 200)
206
- } else this.setProgress()
207
- }
208
- }
209
- },
210
-
211
- created() {
212
- if (import.meta.env.DEV) {
213
- this.checkForErrors()
214
- }
215
- //Declare loading state if no error was detected
216
-
217
- if (!this.error.length) {
218
- this.updateTracker('appBase', 'loading')
219
- this.setAppConfigs(this.appConfig)
220
- }
221
-
222
- window.versionFCAD = this.$helper.getFcadVersionString()
223
- //check if this is running in a mobile environment and register state in the store
224
- const md = new mobileDetect(window.navigator.userAgent)
225
- md.mobile() !== null
226
- ? this.setMobileState(true)
227
- : this.setMobileState(false)
228
- const currentBrowser = this.getBrowser() //get current browser vendor
229
-
230
- // register the current browser in the store
231
- this.setCurrentBrowser(currentBrowser)
232
-
233
- // register device type running the App in the store (ios, Android or Deskop)
234
- this.setDeviceType(this.detectDevice())
235
-
236
- this.$bus.$on('set-comp-status', this.updateTracker)
237
- this.$bus.$on('send-xapi-statement', this.sendXapiStatements)
238
- this.$bus.$on('reset-userdata', this.resetUserData)
239
- this.$bus.$on('fire-exit-event', this.endLesson)
240
- this.$bus.$on('reset-focus-on', this.resetFocus)
241
- this.$bus.$on('move-to-target', this.moveTo)
242
- },
243
- beforeMount() {
244
- window.addEventListener(
245
- 'beforeunload',
246
- (e) => {
247
- this.executeCloseEventTriggered()
248
- // e.preventDefault()
249
- // e.returnValue = true
250
- },
251
- true
252
- )
253
- },
254
-
255
- mounted() {
256
- // set the language of the app
257
- this.setLocale(this.getAppConfigs.lang)
258
- },
259
- beforeUnmount() {
260
- this.$bus.$off('set-comp-status', this.updateTracker)
261
- this.$bus.$off('reset-userdata', this.resetUserData)
262
- this.$bus.$off('fire-exit-event', this.endLesson)
263
- this.$bus.$off('reset-focus-on', this.resetFocus)
264
- this.$bus.$off('send-xapi-statement', this.sendXapiStatements)
265
- this.$bus.$off('move-to-target', this.moveTo)
266
-
267
- if (this.getAppConfigs.remote) this.unsubscribeToSetConfig() //stop watching changing in store to save to local storage
268
- },
269
- methods: {
270
- ...mapActions(useAppStore, [
271
- 'setDeviceType',
272
- 'updateDataFetchFromServer',
273
- 'setUserMetaData',
274
- 'setRouteHistory',
275
- 'setApplicationSettings',
276
- 'setMediaPlaybarValues',
277
- 'updateCompStatusTracker',
278
- 'setAppConfigs',
279
- 'setMobileState',
280
- 'setCurrentBrowser'
281
- ]),
282
- /**
283
- * @description set the desired language for the app default is french
284
- * @param {String} [lang=fr]
285
- */
286
-
287
- setLocale(lang) {
288
- if (!lang) lang = 'fr'
289
- else {
290
- lang = lang.toLowerCase()
291
- if (lang === 'français' || lang === 'francais' || lang === 'french')
292
- lang = 'fr'
293
- else if (lang === 'english' || lang === 'anglais') lang = 'en'
294
- else lang = lang.substring(0, 2).toLowerCase()
295
- this.$i18n.locale = lang
296
- }
297
- },
298
-
299
- getScormState() {
300
- return this.$scorm.GetValue('cmi.suspend_data')
301
- },
302
-
303
- getBrowser() {
304
- let browser
305
- const test = (regexp) => regexp.test(window.navigator.userAgent) //defining the testing fonction
306
-
307
- switch (true) {
308
- case test(/edg/i):
309
- browser = 'Edge'
310
- break
311
- case test(/trident/i):
312
- browser = 'IE'
313
- break
314
- case test(/firefox|fxios/i):
315
- browser = 'Firefox'
316
- break
317
- case test(/opr\//i):
318
- browser = 'Opera'
319
- break
320
- case test(/ucbrowser/i):
321
- browser = 'UC Browser'
322
- break
323
- case test(/samsungbrowser/i):
324
- browser = 'Samsung Browser'
325
- break
326
- case test(/chrome|chromium|crios/i):
327
- if (navigator.brave && navigator.brave.isBrave()) browser = 'Brave'
328
- // as of 2020-11 Brave adds a Class brave in the navigator object
329
- else browser = 'Chrome'
330
- break
331
- case test(/safari/i):
332
- browser = 'Safari'
333
- break
334
- default:
335
- browser = 'Other'
336
- break
337
- }
338
- // Return browser initiale
339
- return browser
340
- },
341
-
342
- detectDevice() {
343
- let device = null
344
- if (
345
- navigator.appVersion.includes('iPad') ||
346
- navigator.appVersion.includes('iPhone')
347
- )
348
- device = 'iOSDevice'
349
- else if (navigator.appVersion.includes('Android'))
350
- device = 'AndroidDevice'
351
- else device = 'Desktop'
352
-
353
- return device
354
- },
355
-
356
- async fetchDataFromServer() {
357
- this.updateTracker('appBase_fetch', 'loading')
358
- if (this.getModuleInfo.packageType !== 'xapi') return
359
- if (!this.getConnectionInfo || !this.getConnectionInfo.remote) return
360
-
361
- const progress = await this.$xapi._getProgress(
362
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
363
- this.getConnectionInfo.activity_id
364
- )
365
- const { routeHistory, ...userProgress } = progress
366
-
367
- const completedState = await this.$xapi._getLessonStatus(
368
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
369
- this.getConnectionInfo.activity_id
370
- )
371
-
372
- //=======UPCOMING: Uncomment lines bellow when these properties are integrated ========
373
- // Get existing record for user preferred settings
374
- // const applicationSettings = await this.$xapi._getPreferredSettings(
375
- // this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
376
- // this.getConnectionInfo.activity_id
377
- // )
378
-
379
- //======= End UPCOMING: Uncomment when this property are integrated ========
380
- //Get existing record for play bar settings. Play bar info is from the activity Parent ID and not activity id
381
- const _url = new URL(this.getConnectionInfo.activity_id)
382
- const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
383
- const playbarValues = await this.$xapi._getPlaybarValues(
384
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
385
- parentID
386
- )
387
- const lessonPosition = await this.$xapi._getLessonPosition(
388
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
389
- this.getConnectionInfo.activity_id
390
- )
391
-
392
- //Update the App Store data
393
- this.updateDataFetchFromServer({
394
- userProgress,
395
- routeHistory,
396
- lessonPosition,
397
- completedState,
398
- playbarValues
399
- })
400
- // console.log('DATA FROM SERVER', {
401
- // userProgress,
402
- // routeHistory,
403
- // lessonPosition,
404
- // completedState,
405
- // playbarValues
406
- // })
407
-
408
- this.updateTracker('appBase_fetch', 'ready')
409
- },
410
-
411
- executeCloseEventTriggered() {
412
- if (
413
- this.getModuleInfo.packageType === 'scorm' &&
414
- this.$scorm.initialized
415
- ) {
416
- this.$bus.$emit('save-to-scorm') // emit event to save to scorm before closing the app
417
- this.$scorm.Finish()
418
- }
419
- //Xapi context
420
- else if (
421
- this.getModuleInfo.packageType === 'xapi' &&
422
- this.getConnectionInfo &&
423
- this.getConnectionInfo.actor &&
424
- this.getConnectionInfo.remote
425
- ) {
426
- this.endLesson(null, true)
427
- }
428
- },
429
-
430
- async setProgress() {
431
- const packageType = this.getModuleInfo.packageType
432
-
433
- switch (packageType) {
434
- case 'scorm':
435
- // Get saved data from suspend_data or from localStorage to update the store:
436
- // LMS is connected
437
- if (this.$scorm.initialized) {
438
- // chacked if there is a existing record in the LMS
439
- if (this.$scorm.GetValue('cmi.suspend_data') !== '') {
440
- const scormRecord = this.$scorm
441
- .GetValue('cmi.suspend_data')
442
- .replace(/\\/g, '')
443
- const userProgress = JSON.parse(scormRecord).userData
444
- const routeHistory = JSON.parse(scormRecord).routeHistory
445
-
446
- this.setUserMetaData(userProgress)
447
- this.setRouteHistory(routeHistory) // update store recored with existing record
448
- }
449
-
450
- // No LMS use LocalStorage record
451
- } else {
452
- this.$idb.openDB().then(() => {
453
- this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
454
- if (res && res.$record) {
455
- const { routeHistory, userSettings, ...userData } =
456
- res.$record
457
- this.setUserMetaData(userData) // update store record with existing record
458
- this.setRouteHistory(routeHistory) // update store record with existing route info
459
- if (userSettings) this.setApplicationSettings(userSettings) // update store record with existing user settings
460
- }
461
- })
462
- })
463
- }
464
-
465
- break
466
-
467
- case 'xapi':
468
- {
469
- if (
470
- this.getConnectionInfo &&
471
- this.getConnectionInfo.actor &&
472
- this.getConnectionInfo.remote
473
- ) {
474
- if (!this.getDataFromServer) return
475
- //Try to get the user progress from LRS
476
- const { playbarValues, routeHistory, userProgress } =
477
- this.getDataFromServer
478
-
479
- this.setUserMetaData(userProgress) // update store record with existing record
480
- this.setRouteHistory(routeHistory) // update store record with existing record
481
-
482
- if (playbarValues) this.setMediaPlaybarValues(playbarValues) // update store record with existing record
483
- //Should update the playbar values
484
- } else {
485
- //Get existing records for user data in local store
486
- this.$idb.openDB().then(() => {
487
- this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
488
- if (res && res.$record) {
489
- const { routeHistory, userSettings, ...userData } =
490
- res.$record
491
-
492
- this.setUserMetaData(userData) // update store record with existing record
493
- this.setRouteHistory(routeHistory) // update store record with existing route info
494
-
495
- if (userSettings) {
496
- this.setApplicationSettings(userSettings) // update store record with existing user setting
497
- }
498
- }
499
- })
500
- })
501
- }
502
- }
503
-
504
- break
505
- }
506
- this.updateTracker('appBase', 'ready')
507
- },
508
- updateTracker(name, status) {
509
- this.updateCompStatusTracker({ name, status })
510
- },
511
- //============================Multiple Satements Sending at once======================================
512
- /**
513
- * @description Send a custom statements to the lrs. Receive some custom params and build a statement to send
514
- * @param {Array} stmts- array of stamtents objects that will be send to the server
515
- * @param {Function} cb
516
- */
517
- async sendXapiStatements(stmts, cb = null, withFetch = true) {
518
- cb = cb || null
519
- if (!stmts) return
520
- let stmtsArray = []
521
-
522
- stmts.constructor === Object
523
- ? stmtsArray.push(stmts)
524
- : (stmtsArray = [...stmts])
525
-
526
- const crsParams = this.getModuleInfo
527
-
528
- if (
529
- this.getConnectionInfo &&
530
- this.getConnectionInfo.actor &&
531
- this.getConnectionInfo.remote
532
- ) {
533
- const stmtsQueue = []
534
-
535
- stmtsArray.forEach((stmtObj) => {
536
- const {
537
- id,
538
- result = null,
539
- definition,
540
- objectType,
541
- type,
542
- description,
543
- verb,
544
- extensions,
545
- duration,
546
- completion
547
- } = stmtObj
548
-
549
- //define the activity id
550
- let activityId
551
- //=========================== activity ID of Object ==========================================
552
- /*
553
- * Define the acitivity id of the stmt according to following:
554
- * The statement is sent at component/ element level : there is Id and Id is not null (id of the element)
555
- * The statement is sent at module level. there is no id
556
- * the Statement must be sent at the course level: There course_id exist and id is the course_id
557
- */
558
-
559
- if (id && id !== crsParams.courseID)
560
- activityId = `${this.getConnectionInfo.activity_id}/${id}`
561
- else if (crsParams.courseID && id === crsParams.courseID)
562
- activityId = this.getConnectionInfo.activity_id.replace(
563
- `/${crsParams.id}`,
564
- ''
565
- )
566
- else activityId = `${this.getConnectionInfo.activity_id}`
567
-
568
- //define the statement object
569
- let stmt = {
570
- actor: this.getConnectionInfo.actor,
571
- verb: (() => {
572
- if (verb && this.$xapi.verbs[verb.trim()])
573
- return this.$xapi.verbs[verb.trim()]
574
- else {
575
- /**
576
- * Determine the verb to use by:
577
- * Checking if the activity is already included in the data fetch from server.
578
- * If not, fetch directly from the server
579
- * There is a found, verb is 'RESUMED'
580
- * There is no found, verb is 'INITIALIZED'
581
- */
582
-
583
- //Regex to test that id contains list of word
584
- const regex =
585
- /(menu|activite_(\d)*|A(\d){2}|conclusion|introduction)$/gm
586
-
587
- switch (true) {
588
- case regex.test(activityId): {
589
- //ID is of activity (menu, conclusion, activite, intro )
590
- const activityStr = activityId.split('/').toReversed()[0]
591
-
592
- if (!this.getDataFromServer)
593
- return this.$xapi.verbs.initialized
594
- const { userProgress } = this.getDataFromServer
595
- let activity_ref
596
- //Define the activity reference
597
- if (['menu', 'introduction', 'A00'].includes(activityStr))
598
- activity_ref = 'A00'
599
- else if (['conclusion', 'A99'].includes(activityStr))
600
- activity_ref = 'A99'
601
- else if (activityStr.includes('activite_')) {
602
- const aNum = activityStr.split('_')[1]
603
- activity_ref = aNum.length > 1 ? `A${aNum}` : `A0${aNum}`
604
- } else activity_ref = activityStr // should be AXX
605
-
606
- if (userProgress && userProgress[activity_ref])
607
- return this.$xapi.verbs.resumed
608
- else return this.$xapi.verbs.initialized
609
- }
610
- default: {
611
- //ID his of Lesson or doesn't exist relate (menu, conclusion, activite, intro )
612
- if (
613
- this.$xapi._getAgent(
614
- this.getConnectionInfo.actor.mbox.replace(
615
- 'mailto:',
616
- ''
617
- ),
618
- activityId
619
- )
620
- )
621
- return this.$xapi.verbs.resumed
622
- else return this.$xapi.verbs.initialized
623
- }
624
- }
625
- }
626
- })(),
627
- object: {
628
- id: activityId,
629
- definition: {
630
- name: {
631
- [this.displayLang]: `${definition}`
632
- },
633
- description: {
634
- [this.displayLang]: `${description}`,
635
- type: type || 'http://activitystrea.ms/schema/1.0/page'
636
- }
637
- },
638
- objectType: objectType || 'Activity'
639
- },
640
- context: {
641
- contextActivities: {}
642
- }
643
- }
644
- //===================== contextActivity parent =====================//
645
- /*
646
- Define parent in the contextActivity
647
- When:
648
- 1- when we have the id (parent === module)
649
- 2- when with have no id but we have a course_id (parent is cours_id)
650
- */
651
- const activityParent = (() => {
652
- if (crsParams.courseID && !id)
653
- return {
654
- id: this.getConnectionInfo.activity_id.replace(
655
- `/${crsParams.id}`,
656
- ''
657
- ),
658
- objectType: objectType || 'Activity'
659
- }
660
- else if (id && id !== crsParams.courseID)
661
- return {
662
- id: `${this.getConnectionInfo.activity_id}`,
663
- objectType: objectType || 'Activity'
664
- }
665
- else return null
666
- })()
667
-
668
- // Add the parent key of the context activity
669
- if (activityParent)
670
- stmt.context.contextActivities['parent'] = [activityParent]
671
-
672
- //===================== contextActivity grouping =====================//
673
-
674
- //Defining the Grouping of the context activity
675
- const activityGrouping = (() => {
676
- if (
677
- activityParent &&
678
- crsParams.courseID &&
679
- activityParent.id.includes(crsParams.id)
680
- )
681
- return {
682
- id: this.getConnectionInfo.activity_id.replace(
683
- `/${crsParams.id}`,
684
- ''
685
- )
686
- }
687
- else return null
688
- })()
689
- //Adding the grouping of the context activity
690
- if (activityGrouping)
691
- stmt.context.contextActivities['grouping'] = [activityGrouping]
692
-
693
- //add result data to statement
694
- if (result) stmt['result'] = result
695
-
696
- // Add duration info to statement when activity is complete
697
- if (['completed', 'suspended', 'terminated'].includes(verb)) {
698
- if (!stmt.result) stmt.result = {} // check if exist
699
-
700
- let d
701
-
702
- duration
703
- ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
704
- duration.split(':')[2]
705
- }S`)
706
- : (d = `PT${this.activityDuration.split(':')[0]}H${
707
- this.activityDuration.split(':')[1]
708
- }M${this.activityDuration.split(':')[2]}S`)
709
-
710
- stmt.result['duration'] = d
711
- //Set the completion status of the result
712
- if (completion) stmt.result['completion'] = completion
713
- }
714
-
715
- // ===================== Extension of the Object definition =====================//
716
- if (
717
- extensions &&
718
- extensions.constructor === Array &&
719
- extensions.length > 0
720
- ) {
721
- //Validate each entry given in the extension Array
722
- extensions.forEach((e) => {
723
- //entry must be of type Object
724
- if (e.constructor !== Object)
725
- throw new Error(`'${e}' is not a valid value. Must be Object`)
726
-
727
- //Entry Must have id and content keys
728
- const validKey = ['id', 'content']
729
- Object.keys(e).forEach((key) => {
730
- if (!validKey.includes(key))
731
- throw new Error(`Not valid key '${key}' for entry ${e}`)
732
-
733
- //id must be a String
734
- if (key === 'id' && e[key] && e[key].constructor !== String)
735
- throw new Error(`'${key}' must be of type String`)
736
- })
737
-
738
- stmt.object.definition['extensions'] = {
739
- ...stmt.object.definition['extensions'],
740
- [`${this.getConnectionInfo.activity_id}/${e.id}`]: e.content
741
- }
742
- })
743
- }
744
-
745
- //============================================================
746
- stmtsQueue.push(stmt)
747
- })
748
-
749
- this.$xapi._sendStatements(stmtsQueue, cb, withFetch)
750
- }
751
- },
752
-
753
- /**
754
- * @param {Function} cb
755
- * @param {Bool} option - set if must use Fetch API or not. default = false
756
- */
757
- endLesson(cb, option = true) {
758
- cb = cb || null
759
- let text
760
- //Defining the text to display for stmt description and definition
761
- switch (this.$i18n.locale) {
762
- case 'fr':
763
- if (this.getModuleInfo.courseID)
764
- text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
765
- else text = `Le ${this.getModuleInfo.id}`
766
- break
767
- case 'en':
768
- if (this.getModuleInfo.courseID)
769
- text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
770
- else text = `The ${this.getModuleInfo.id}`
771
- break
772
- }
773
-
774
- // Retrieve only user data in lessons front its interaction that we want to send to the LRS.
775
- // Note: User Settings are sent on a different URI and statement
776
- const { isFistTime, userSettings, ...lessonsData } =
777
- this.getUserInteraction
778
-
779
- const stmt = {
780
- verb: 'suspended',
781
- definition: text,
782
- description: text,
783
- extensions: [
784
- {
785
- id: 'ending-point',
786
- content: this.$route.name
787
- },
788
- {
789
- id: 'user-data',
790
- content: {
791
- routeHistory: this.getRouteHistory,
792
- ...lessonsData
793
- }
794
- }
795
- ],
796
- duration: this.lessonDuration
797
- }
798
-
799
- const endStmt = { ...stmt }
800
- endStmt.verb = 'terminated'
801
- //================================STATEMENT FOR THE PLAYBAR ===============================================
802
- //Creating custom statement
803
- const stmtPlaybar = {
804
- id: (() => {
805
- if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
806
- else return null
807
- })(),
808
- verb: 'played',
809
- definition: text,
810
- description: text,
811
- extensions: [
812
- {
813
- id: 'playbar-values',
814
- content: {
815
- ...this.getMediaPlaybarValues()
816
- }
817
- }
818
- ]
819
- }
820
- //================================STATEMENT FOR THE PLAYBAR ===============================================
821
-
822
- this.sendXapiStatements([stmtPlaybar, stmt, endStmt], null, option) //send xapi statement
823
-
824
- if (this.timerState === 'started') setTimeout(() => this.stopTimer(), 0) //clear the timer
825
-
826
- if (cb) cb()
827
- },
828
-
829
- /** @description Method to reset the progression status of user
830
- * the function reset first the user data in the app store then
831
- * reset the idbStore if APP is running locally or prepare a statement with new userData Object to be sent over the LRS
832
- * for the connected userAgent, activity if APP is Running online
833
- * @event 'send-xapi-statement' emit to AppBaseModule.vue that will be executed by sendXapiStatement(s)
834
- */
835
- async resetUserData() {
836
- this.$bus.$emit('set-comp-status', 'appBase', 'loading')
837
- this.setUserMetaData({}) // resetting store record with existing for user data
838
- this.setRouteHistory([]) // resetting store record for all last visited pages
839
-
840
- this.$bus.$emit('stop-timer')
841
- if (this.getModuleInfo.packageType !== 'xapi') return
842
-
843
- if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
844
- return this.$idb.deleteDataInDB(this.getModuleInfo.idbID) //Must call the idb delete methode to reset indexDB store
845
-
846
- //Send a completion statement for the current activity
847
- let aTitle = `${this.$t('text.activity')} ${this.getConnectionInfo.activity_id}`
848
-
849
- //Custom text for description and definition of the stament to be sent
850
- let text
851
- this.$i18n.locale == 'fr'
852
- ? (text = `L'${aTitle} de ${this.getModuleInfo.id}`)
853
- : (text = `The ${aTitle} of ${this.getModuleInfo.id}`)
854
-
855
- /*Dispatch a send statement event to method AppBaseModule sendXapiStatement(s)
856
- Content values are:
857
- User data URI: "host_address/course_ID/Lesson_ID/user-data" ex: "http://localhost:8080/330N01FD6001/m1l1/ // user-data" and
858
- User Bookmark URI: "host_address/course_ID/Lesson_ID/ending-point" ex: ""http://localhost:8080/330N01FD6001/m1l1/ending-point": "menu"
859
- */
860
- const baseStmt = {
861
- definition: text,
862
- description: text
863
- }
864
- const exitStmt = {
865
- ...baseStmt,
866
- verb: 'exited'
867
- }
868
-
869
- const endStmt = {
870
- ...baseStmt,
871
- verb: 'terminated',
872
- extensions: [
873
- {
874
- id: 'ending-point',
875
- content: this.$route.name
876
- },
877
- {
878
- id: 'user-data',
879
- content: new Object()
880
- }
881
- ],
882
- duration: null,
883
- completion: false // Resetting completion state value to
884
- }
885
-
886
- this.sendXapiStatements([exitStmt, endStmt])
887
-
888
- this.$bus.$emit('set-comp-status', 'appBase', 'ready')
889
-
890
- // Creating a asynchronous fecth method that would resolve after a 2 second
891
- const fetchData = async () => {
892
- return new Promise((resolve) => {
893
- setTimeout(
894
- () =>
895
- resolve(
896
- this.$xapi._getProgress(
897
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
898
- this.getConnectionInfo.activity_id
899
- )
900
- ),
901
- 2000
902
- )
903
- })
904
- }
905
-
906
- // Make request over user current data after sending statement to assure that the resetting worked
907
- const usrData = await fetchData()
908
- this.$bus.$emit('reset-complete')
909
- console.log('😄 USER STATUS: ', {
910
- User: this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
911
- ActivityID: this.getConnectionInfo.activity_id,
912
- Record: usrData
913
- })
914
- },
915
-
916
- /**
917
- * @description- Scroll directly to the target* containt
918
- * target content can be any Node defined by the ID
919
- * @param {String} target = id of HTMLElement. if target don't existe will scroll to wrapper-content
920
- * @param {Obj} opt = scroll behavior to apply default { top: 0, left: 0, behavior: 'auto' }
921
- */
922
-
923
- moveTo(target, opt = { top: 0, left: 0, behavior: 'auto' }) {
924
- if (target.constructor !== String)
925
- throw new Error(
926
- '⚠️ Not supported value for @target. Must be of type String'
927
- )
928
-
929
- let skipTo = document.querySelector(`#wrapper-content`) //default definition of main element
930
-
931
- if (target) {
932
- let targetEl = document.querySelector(`#${target}`) // search for node element specified as main
933
-
934
- if (targetEl) skipTo = targetEl
935
- }
936
-
937
- if (skipTo) opt.top = skipTo.offsetTop + opt.top // Set scroll top from target top
938
-
939
- if (skipTo) skipTo.setAttribute('tabIndex', -1) //Allowing accessibility control with keyboard
940
- window.scrollTo(opt)
941
- if (skipTo) this.resetFocus(skipTo) //focus on the target
942
- },
943
-
944
- /**
945
- * @description Reset the focus the element
946
- * @param {HTMLElement} e - element that will get focus
947
- */
948
- resetFocus(e) {
949
- if (e) e.focus()
950
- },
951
- /**
952
- * @description Mothods to validate that application settings are correctly
953
- * Opens error component when configurations are not correct
954
- *
955
- */
956
- checkForErrors() {
957
- let errMessage
958
-
959
- if (validateAppContent(this.appConfig, true).length) {
960
- return (this.error = validateAppContent(this.appConfig, true))
961
- }
962
- const { list: activitiesList } = this.getAllActivities()
963
- let { no_menu, is_single_activity } = this.appConfig
964
-
965
- let noMenu = no_menu == undefined ? false : no_menu
966
-
967
- let isSingleActivity =
968
- is_single_activity == undefined ? false : is_single_activity
969
-
970
- let err
971
- if (this.getErrorMenu) err = true
972
- else err = false
973
-
974
- let consoleMsg = ''
975
- //error if There is more than one activity when is_single_activity
976
- switch (true) {
977
- case isSingleActivity && activitiesList.size > 1:
978
- errMessage = `La configuration choisie ne permet pas d'avoir plus d'une activité dans l'application. \n Vous devez soit désactiver l'option 💲<b>is_single_activity</b> ou retirer TOUTES les autres activités`
979
-
980
- consoleMsg = `Cannot have more than 1 activity with this settings configuration. Either disable 💲is_single_activity or DELETE ALL others activities`
981
- this.error.push(errMessage)
982
- break
983
-
984
- case isSingleActivity && !noMenu:
985
- errMessage = `Le MENU n'est pas disponible avec la configuration choisie.\n Vous devez soit désactiver l'option 💲<b>no_menu</b> ou l'option 💲<b>is_single_activity</b>`
986
- consoleMsg = `Cannot have MENU with current settings configuration. Either set 💲no_menu:false or 💲is_single_activity:false`
987
- this.error.push(errMessage)
988
- break
989
-
990
- case err:
991
- errMessage = `Il y une erreur dans votre fichier menu.setting.js. \n ouvrez votre console pour voir l'erreur et corriger la dans menu.setting.js `
992
- consoleMsg = this.getErrorMenu
993
- this.error.push(errMessage)
994
- break
995
- }
996
-
997
- if (errMessage)
998
- return console.warn(
999
- `%c WARNING!>>> ${consoleMsg}`,
1000
- 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
1001
- )
1002
- }
1003
- }
1004
- }
1005
- </script>
1006
- <style lang="scss">
1007
- #App-base {
1008
- width: 100%;
1009
- height: 100%;
1010
- min-height: 100vh;
1011
-
1012
- #overlay_loading {
1013
- position: fixed !important;
1014
- width: 100%;
1015
- height: 100%;
1016
- top: 0;
1017
- left: 0;
1018
- z-index: 9999;
1019
- }
1020
-
1021
- #build-info {
1022
- opacity: 0.9;
1023
- position: fixed;
1024
- right: 0;
1025
- bottom: 0;
1026
- color: #fff;
1027
- text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4);
1028
- background-color: hotpink;
1029
- font-family: serif, Impact, Arial;
1030
- font-size: 11px;
1031
- padding: 3px;
1032
- cursor: pointer;
1033
- z-index: 999;
1034
- span {
1035
- text-align: center;
1036
- display: block;
1037
- }
1038
- }
1039
-
1040
- .box {
1041
- width: 100%;
1042
- height: 100%;
1043
- position: relative;
1044
- }
1045
- }
1046
- .grp-spinners {
1047
- span {
1048
- margin: 0px 2px;
1049
- }
1050
- .v-progress-circular {
1051
- margin: 0.8rem !important;
1052
- }
1053
- }
1054
- </style>
1
+ <!--
2
+ @ Description: This is a root component to create the App
3
+ @ What it does:
4
+ - validate the data for the App configuration
5
+ - Send the xapi statement
6
+ - Manage the fetching and setting of data from serveur
7
+ - Example of use: <app-base :app-config="$data"></app-base>
8
+ @ Must be used.
9
+ -->
10
+ <template>
11
+ <div id="App-base" fluid :class="{ iPad: resizeiPad }">
12
+ <template v-if="error.length">
13
+ <v-row>
14
+ <v-col>
15
+ <app-base-error-display
16
+ :errors-list="error"
17
+ error-type="appConfig"
18
+ error-title="Configuration de l'application"
19
+ />
20
+ </v-col>
21
+ </v-row>
22
+ </template>
23
+ <template v-else>
24
+ <transition name="bounce" mode="in-out">
25
+ <div
26
+ v-if="showBuildInfo && !buildInfoClicked"
27
+ id="build-info"
28
+ @click="buildInfoClicked = true"
29
+ >
30
+ <span>{{ getModuleInfo.courseID.toUpperCase() }}</span>
31
+ <span>FCAD {{ $helper.getFcadVersion() }}</span>
32
+ <span>template {{ $helper.getTemplateVersion() }}</span>
33
+ <span>{{ $helper.getBuildTime() }}</span>
34
+ </div>
35
+ </transition>
36
+
37
+ <router-view class="box" />
38
+
39
+ <app-icons-next :extra-icons="userExtraIcons" />
40
+ </template>
41
+
42
+ <v-overlay
43
+ id="overlay_loading"
44
+ :model-value="!appReady && initialLoading"
45
+ class="align-center justify-center"
46
+ scrim="white"
47
+ opacity="0.8"
48
+ >
49
+ <div class="text-center grp-spinners">
50
+ <v-icon id="icon_loading" icon="mdi-dots-circle" size="x-large" />
51
+
52
+ <span class="sr-only">
53
+ {{ $t('message.loading_state_msg') }}
54
+ </span>
55
+ </div>
56
+ </v-overlay>
57
+ </div>
58
+ </template>
59
+ <script>
60
+ import { mapState, mapActions } from 'pinia'
61
+ import { useAppStore } from '../module/stores/appStore.js'
62
+ import { timerMixin } from '../mixins/timerMixin'
63
+ import { validateAppContent } from '../shared/validators'
64
+
65
+ import mobileDetect from 'mobile-detect'
66
+ export default {
67
+ mixins: [timerMixin],
68
+ props: {
69
+ appConfig: {
70
+ type: Object,
71
+ required: true,
72
+ validator: (value) => {
73
+ if (import.meta.env.DEV) return validateAppContent(value).length === 0
74
+ }
75
+ }
76
+ },
77
+ setup() {
78
+ const store = useAppStore()
79
+ return { store }
80
+ },
81
+
82
+ data() {
83
+ return {
84
+ randKey: null,
85
+ isLandScape: null,
86
+ initialDeviceOrentation: null,
87
+ appIsFullScreen: false,
88
+ resizeiPad: false,
89
+ buildInfoClicked: false,
90
+ error: [],
91
+ initialLoading: true
92
+ }
93
+ },
94
+ computed: {
95
+ ...mapState(useAppStore, [
96
+ 'getCurrentBrowser',
97
+ 'getIsMobile',
98
+ 'getDeviceType',
99
+ 'getModuleInfo',
100
+ 'getAppStatus',
101
+ 'getUserInteraction',
102
+ 'getConnectionInfo',
103
+ 'getDataFromServer',
104
+ 'getAllActivities',
105
+ 'getMediaPlaybarValues',
106
+ 'getAppConfigs',
107
+ 'getErrorMenu',
108
+ 'getRouteHistory',
109
+ 'getBookmarkEnabled'
110
+ ]),
111
+ getwidth() {
112
+ return window.innerWidth
113
+ },
114
+ getheight() {
115
+ return window.innerHeight
116
+ },
117
+
118
+ showBuildInfo() {
119
+ return (
120
+ import.meta.env.PROD &&
121
+ window.location.host === 'projets.cegepadistance.ca'
122
+ )
123
+ },
124
+ appReady() {
125
+ let readyState = this.getAppStatus === 'ready' ? true : false
126
+ if (readyState) {
127
+ this.setInitialLoadingDone()
128
+ }
129
+
130
+ return readyState
131
+ },
132
+ displayLang() {
133
+ let lang = false
134
+ const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
135
+
136
+ if (this.getModuleInfo.packageType === 'xapi') {
137
+ lang = displayList.find((l) => l.includes(this.$i18n.locale))
138
+ }
139
+ return lang
140
+ },
141
+ userExtraIcons() {
142
+ const icons = this.$helper.getSettingsFromStore('extra_icons')
143
+ ? this.$helper.getSettingsFromStore('extra_icons')
144
+ : null
145
+ return icons
146
+ } /**
147
+ * @description Set default value for bookmark
148
+ */,
149
+ bookmarkActive() {
150
+ return this.getBookmarkEnabled
151
+ }
152
+ },
153
+ watch: {
154
+ getConnectionInfo: {
155
+ deep: true,
156
+ //in development environment (localhost), don't wait for the axios call
157
+ immediate: import.meta.env.DEV,
158
+ handler() {
159
+ /**
160
+ * Attach listener to detect when user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile,
161
+ * switches from the browser to a different app To save user data to LRS/LMS.
162
+ * As of 06/2021 MDN Web Doc advise preferring the 'visibilitychange' event over 'unload/beforeunload'
163
+ * event to send data over a serveur.
164
+ * Ref:https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
165
+ * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
166
+ */
167
+
168
+ if (!this.getConnectionInfo) return
169
+ if (this.getIsMobile) {
170
+ document.addEventListener(
171
+ 'visibilitychange',
172
+ () => {
173
+ if (document.visibilityState === 'hidden') {
174
+ this.executeCloseEventTriggered()
175
+ }
176
+ },
177
+ false
178
+ )
179
+ }
180
+ // initialize scorm Commuinication with LMS || xAPI LRS
181
+ if (this.getModuleInfo.packageType === 'scorm') this.$scorm.Initialize()
182
+ else if (
183
+ this.getModuleInfo.packageType === 'xapi' &&
184
+ this.getConnectionInfo &&
185
+ this.getConnectionInfo.actor &&
186
+ this.getConnectionInfo.remote
187
+ ) {
188
+ const {
189
+ auth,
190
+ endpoint,
191
+ registration = this.$xapi.ruuid()
192
+ } = this.getConnectionInfo
193
+
194
+ const config = {
195
+ auth,
196
+ endpoint,
197
+ registration,
198
+ activity_platform: `SIPI_organizationId`
199
+ }
200
+
201
+ this.$xapi._configLRS(config) // configure and launch LRS
202
+
203
+ setTimeout(() => {
204
+ this.fetchDataFromServer().then(() => {
205
+ setTimeout(() => {
206
+ this.setProgress()
207
+ }, 200)
208
+ })
209
+ }, 200)
210
+ } else this.setProgress()
211
+ }
212
+ }
213
+ },
214
+
215
+ created() {
216
+ this.bookmarkActive
217
+ if (import.meta.env.DEV) {
218
+ this.checkForErrors()
219
+ }
220
+ //Declare loading state if no error was detected
221
+
222
+ if (!this.error.length) {
223
+ this.updateTracker('appBase', 'loading')
224
+ this.setAppConfigs(this.appConfig)
225
+ }
226
+
227
+ window.versionFCAD = this.$helper.getFcadVersionString()
228
+ //check if this is running in a mobile environment and register state in the store
229
+ const md = new mobileDetect(window.navigator.userAgent)
230
+ md.mobile() !== null
231
+ ? this.setMobileState(true)
232
+ : this.setMobileState(false)
233
+ const currentBrowser = this.getBrowser() //get current browser vendor
234
+
235
+ // register the current browser in the store
236
+ this.setCurrentBrowser(currentBrowser)
237
+
238
+ // register device type running the App in the store (ios, Android or Deskop)
239
+ this.setDeviceType(this.detectDevice())
240
+
241
+ this.$bus.$on('set-comp-status', this.updateTracker)
242
+ this.$bus.$on('send-xapi-statement', this.sendXapiStatements)
243
+ this.$bus.$on('reset-userdata', this.resetUserData)
244
+ this.$bus.$on('fire-exit-event', this.endLesson)
245
+ this.$bus.$on('reset-focus-on', this.resetFocus)
246
+ this.$bus.$on('move-to-target', this.moveTo)
247
+ },
248
+ beforeMount() {
249
+ window.addEventListener(
250
+ 'beforeunload',
251
+ (e) => {
252
+ this.executeCloseEventTriggered()
253
+ // e.preventDefault()
254
+ // e.returnValue = true
255
+ },
256
+ true
257
+ )
258
+ },
259
+
260
+ mounted() {
261
+ // set the language of the app
262
+ this.setLocale(this.getAppConfigs.lang)
263
+ },
264
+ beforeUnmount() {
265
+ this.$bus.$off('set-comp-status', this.updateTracker)
266
+ this.$bus.$off('reset-userdata', this.resetUserData)
267
+ this.$bus.$off('fire-exit-event', this.endLesson)
268
+ this.$bus.$off('reset-focus-on', this.resetFocus)
269
+ this.$bus.$off('send-xapi-statement', this.sendXapiStatements)
270
+ this.$bus.$off('move-to-target', this.moveTo)
271
+
272
+ if (this.getAppConfigs.remote) this.unsubscribeToSetConfig() //stop watching changing in store to save to local storage
273
+ },
274
+ methods: {
275
+ ...mapActions(useAppStore, [
276
+ 'setDeviceType',
277
+ 'updateDataFetchFromServer',
278
+ 'setUserMetaData',
279
+ 'setRouteHistory',
280
+ 'setApplicationSettings',
281
+ 'setMediaPlaybarValues',
282
+ 'updateCompStatusTracker',
283
+ 'setAppConfigs',
284
+ 'setMobileState',
285
+ 'setCurrentBrowser'
286
+ ]),
287
+ setInitialLoadingDone() {
288
+ this.initialLoading = false
289
+ },
290
+ /**
291
+ * @description set the desired language for the app default is french
292
+ * @param {String} [lang=fr]
293
+ */
294
+
295
+ setLocale(lang) {
296
+ if (!lang) lang = 'fr'
297
+ else {
298
+ lang = lang.toLowerCase()
299
+ if (lang === 'français' || lang === 'francais' || lang === 'french')
300
+ lang = 'fr'
301
+ else if (lang === 'english' || lang === 'anglais') lang = 'en'
302
+ else lang = lang.substring(0, 2).toLowerCase()
303
+ this.$i18n.locale = lang
304
+ }
305
+ },
306
+
307
+ getScormState() {
308
+ return this.$scorm.GetValue('cmi.suspend_data')
309
+ },
310
+
311
+ getBrowser() {
312
+ let browser
313
+ const test = (regexp) => regexp.test(window.navigator.userAgent) //defining the testing fonction
314
+
315
+ switch (true) {
316
+ case test(/edg/i):
317
+ browser = 'Edge'
318
+ break
319
+ case test(/trident/i):
320
+ browser = 'IE'
321
+ break
322
+ case test(/firefox|fxios/i):
323
+ browser = 'Firefox'
324
+ break
325
+ case test(/opr\//i):
326
+ browser = 'Opera'
327
+ break
328
+ case test(/ucbrowser/i):
329
+ browser = 'UC Browser'
330
+ break
331
+ case test(/samsungbrowser/i):
332
+ browser = 'Samsung Browser'
333
+ break
334
+ case test(/chrome|chromium|crios/i):
335
+ if (navigator.brave && navigator.brave.isBrave()) browser = 'Brave'
336
+ // as of 2020-11 Brave adds a Class brave in the navigator object
337
+ else browser = 'Chrome'
338
+ break
339
+ case test(/safari/i):
340
+ browser = 'Safari'
341
+ break
342
+ default:
343
+ browser = 'Other'
344
+ break
345
+ }
346
+ // Return browser initiale
347
+ return browser
348
+ },
349
+
350
+ detectDevice() {
351
+ let device = null
352
+ if (
353
+ navigator.appVersion.includes('iPad') ||
354
+ navigator.appVersion.includes('iPhone')
355
+ )
356
+ device = 'iOSDevice'
357
+ else if (navigator.appVersion.includes('Android'))
358
+ device = 'AndroidDevice'
359
+ else device = 'Desktop'
360
+
361
+ return device
362
+ },
363
+
364
+ async fetchDataFromServer() {
365
+ this.updateTracker('appBase_fetch', 'loading')
366
+ if (this.getModuleInfo.packageType !== 'xapi') return
367
+ if (!this.getConnectionInfo || !this.getConnectionInfo.remote) return
368
+
369
+ const progress = await this.$xapi._getProgress(
370
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
371
+ this.getConnectionInfo.activity_id
372
+ )
373
+ const { routeHistory = [], ...userProgress } = progress
374
+
375
+ const completedState = await this.$xapi._getLessonStatus(
376
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
377
+ this.getConnectionInfo.activity_id
378
+ )
379
+
380
+ const lessonPosition = await this.$xapi._getLessonPosition(
381
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
382
+ this.getConnectionInfo.activity_id
383
+ )
384
+ //======= End UPCOMING: Uncomment when this property are integrated ========
385
+ //Get existing record for play bar settings. Play bar info is from the activity Parent ID and not activity id
386
+ const _url = new URL(this.getConnectionInfo.activity_id)
387
+ const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
388
+ const playbarValues = await this.$xapi._getPlaybarValues(
389
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
390
+ parentID
391
+ )
392
+
393
+ //=======UPCOMING: Uncomment lines bellow when these properties are integrated ========
394
+ // Get existing record for user preferred settings
395
+ const applicationSettings = await this.$xapi._getPreferredSettings(
396
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
397
+ parentID
398
+ )
399
+ //Update the App Store data
400
+ this.updateDataFetchFromServer({
401
+ userProgress,
402
+ routeHistory,
403
+ lessonPosition,
404
+ completedState,
405
+ playbarValues,
406
+ applicationSettings
407
+ }).then(() => {
408
+ // console.log('DATA FROM SERVER', {
409
+ // userProgress,
410
+ // routeHistory,
411
+ // lessonPosition,
412
+ // completedState,
413
+ // playbarValues,
414
+ // applicationSettings
415
+ // })// for testing
416
+
417
+ this.updateTracker('appBase_fetch', 'ready')
418
+ })
419
+ },
420
+
421
+ executeCloseEventTriggered() {
422
+ if (
423
+ this.getModuleInfo.packageType === 'scorm' &&
424
+ this.$scorm.initialized
425
+ ) {
426
+ this.$bus.$emit('save-to-scorm') // emit event to save to scorm before closing the app
427
+ this.$scorm.Finish()
428
+ }
429
+ //Xapi context
430
+ else if (
431
+ this.getModuleInfo.packageType === 'xapi' &&
432
+ this.getConnectionInfo &&
433
+ this.getConnectionInfo.actor &&
434
+ this.getConnectionInfo.remote
435
+ ) {
436
+ this.endLesson(null, true)
437
+ }
438
+ },
439
+
440
+ async setProgress() {
441
+ const packageType = this.getModuleInfo.packageType
442
+
443
+ switch (packageType) {
444
+ case 'scorm':
445
+ // Get saved data from suspend_data or from localStorage to update the store:
446
+ // LMS is connected
447
+ if (this.$scorm.initialized) {
448
+ // chacked if there is a existing record in the LMS
449
+ if (this.$scorm.GetValue('cmi.suspend_data') !== '') {
450
+ const scormRecord = this.$scorm
451
+ .GetValue('cmi.suspend_data')
452
+ .replace(/\\/g, '')
453
+ const userProgress = JSON.parse(scormRecord).userData
454
+ const routeHistory = JSON.parse(scormRecord).routeHistory
455
+
456
+ this.setUserMetaData(userProgress)
457
+ this.setRouteHistory(routeHistory) // update store recored with existing record
458
+ //Should Redirect to the last Route in History
459
+ // const lastRoute = routeHistory.toReversed()[0]
460
+ // this.$router.push({ name: lastRoute.parent._namedRoute })
461
+ }
462
+
463
+ // No LMS use LocalStorage record
464
+ } else {
465
+ this.$idb.openDB().then(() => {
466
+ this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
467
+ if (res && res.$record) {
468
+ const { routeHistory, userSettings, progress } = res.$record
469
+ this.setUserMetaData(...progress) // update store record with existing record
470
+ this.setRouteHistory(routeHistory) // update store record with existing route info
471
+ if (userSettings) this.setApplicationSettings(userSettings) // update store record with existing user settings
472
+ }
473
+ })
474
+ })
475
+ }
476
+
477
+ break
478
+
479
+ case 'xapi':
480
+ {
481
+ if (
482
+ this.getConnectionInfo &&
483
+ this.getConnectionInfo.actor &&
484
+ this.getConnectionInfo.remote
485
+ ) {
486
+ if (!this.getDataFromServer) return
487
+ //Try to get the user progress from LRS
488
+ const {
489
+ playbarValues,
490
+ routeHistory,
491
+ userProgress,
492
+ lessonPosition,
493
+ applicationSettings
494
+ } = this.getDataFromServer
495
+
496
+ this.setUserMetaData(userProgress) // update store record with existing record
497
+ this.setRouteHistory(routeHistory) // update store record with existing record
498
+
499
+ if (playbarValues) this.setMediaPlaybarValues(playbarValues) // update store record with existing record
500
+
501
+ if (applicationSettings)
502
+ this.setApplicationSettings(applicationSettings) // set the user preferred settings
503
+
504
+ //Handel bookmark navigation
505
+ if (this.bookmarkActive && lessonPosition)
506
+ this.$router.push({ name: lessonPosition }) //Should Redirect to bookmark
507
+ } else {
508
+ //Get existing records for user data in local store
509
+ this.$idb.openDB().then(() => {
510
+ this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
511
+ if (res && res.$record) {
512
+ const { routeHistory, userSettings, progress } = res.$record
513
+ this.setUserMetaData(progress) // update store record with existing record
514
+ this.setRouteHistory(routeHistory) // update store record with existing route info
515
+
516
+ if (userSettings) {
517
+ this.setApplicationSettings(userSettings) // update store record with existing user setting
518
+ }
519
+ }
520
+ })
521
+ })
522
+ }
523
+ }
524
+
525
+ break
526
+ }
527
+ this.updateTracker('appBase', 'ready')
528
+ },
529
+ updateTracker(name, status) {
530
+ this.updateCompStatusTracker({ name, status })
531
+ },
532
+ //============================Multiple Satements Sending at once======================================
533
+ /**
534
+ * @description Send a custom statements to the lrs. Receive some custom params and build a statement to send
535
+ * @param {Array} stmts- array of stamtents objects that will be send to the server
536
+ * @param {Function} cb
537
+ */
538
+ async sendXapiStatements(stmts, cb = null, withFetch = true) {
539
+ cb = cb || null
540
+ if (!stmts) return
541
+ let stmtsArray = []
542
+
543
+ stmts.constructor === Object
544
+ ? stmtsArray.push(stmts)
545
+ : (stmtsArray = [...stmts])
546
+
547
+ const crsParams = this.getModuleInfo
548
+
549
+ if (
550
+ this.getConnectionInfo &&
551
+ this.getConnectionInfo.actor &&
552
+ this.getConnectionInfo.remote
553
+ ) {
554
+ const stmtsQueue = []
555
+
556
+ stmtsArray.forEach((stmtObj) => {
557
+ const {
558
+ id,
559
+ result = null,
560
+ definition,
561
+ objectType,
562
+ type,
563
+ description,
564
+ verb,
565
+ extensions,
566
+ duration,
567
+ completion
568
+ } = stmtObj
569
+
570
+ //define the activity id
571
+ let activityId
572
+ //=========================== activity ID of Object ==========================================
573
+ /*
574
+ * Define the acitivity id of the stmt according to following:
575
+ * The statement is sent at component/ element level : there is Id and Id is not null (id of the element)
576
+ * The statement is sent at module level. there is no id
577
+ * the Statement must be sent at the course level: There course_id exist and id is the course_id
578
+ */
579
+
580
+ if (id && id !== crsParams.courseID)
581
+ activityId = `${this.getConnectionInfo.activity_id}/${id}`
582
+ else if (crsParams.courseID && id === crsParams.courseID)
583
+ activityId = this.getConnectionInfo.activity_id.replace(
584
+ `/${crsParams.id}`,
585
+ ''
586
+ )
587
+ else activityId = `${this.getConnectionInfo.activity_id}`
588
+
589
+ //define the statement object
590
+ let stmt = {
591
+ actor: this.getConnectionInfo.actor,
592
+ verb: (() => {
593
+ if (verb && this.$xapi.verbs[verb.trim()])
594
+ return this.$xapi.verbs[verb.trim()]
595
+ else {
596
+ /**
597
+ * Determine the verb to use by:
598
+ * Checking if the activity is already included in the data fetch from server.
599
+ * If not, fetch directly from the server
600
+ * There is a found, verb is 'RESUMED'
601
+ * There is no found, verb is 'INITIALIZED'
602
+ */
603
+
604
+ //Regex to test that id contains list of word
605
+ const regex =
606
+ /(menu|activite_(\d)*|A(\d){2}|conclusion|introduction)$/gm
607
+
608
+ switch (true) {
609
+ case regex.test(activityId): {
610
+ //ID is of activity (menu, conclusion, activite, intro )
611
+ const activityStr = activityId.split('/').toReversed()[0]
612
+
613
+ if (!this.getDataFromServer)
614
+ return this.$xapi.verbs.initialized
615
+ const { userProgress } = this.getDataFromServer
616
+ let activity_ref
617
+ //Define the activity reference
618
+ if (['menu', 'introduction', 'A00'].includes(activityStr))
619
+ activity_ref = 'A00'
620
+ else if (['conclusion', 'A99'].includes(activityStr))
621
+ activity_ref = 'A99'
622
+ else if (activityStr.includes('activite_')) {
623
+ const aNum = activityStr.split('_')[1]
624
+ activity_ref = aNum.length > 1 ? `A${aNum}` : `A0${aNum}`
625
+ } else activity_ref = activityStr // should be AXX
626
+
627
+ if (userProgress && userProgress[activity_ref])
628
+ return this.$xapi.verbs.resumed
629
+ else return this.$xapi.verbs.initialized
630
+ }
631
+ default: {
632
+ //ID his of Lesson or doesn't exist relate (menu, conclusion, activite, intro )
633
+ if (
634
+ this.$xapi._getAgent(
635
+ this.getConnectionInfo.actor.mbox.replace(
636
+ 'mailto:',
637
+ ''
638
+ ),
639
+ activityId
640
+ )
641
+ )
642
+ return this.$xapi.verbs.resumed
643
+ else return this.$xapi.verbs.initialized
644
+ }
645
+ }
646
+ }
647
+ })(),
648
+ object: {
649
+ id: activityId,
650
+ definition: {
651
+ name: {
652
+ [this.displayLang]: `${definition}`
653
+ },
654
+ description: {
655
+ [this.displayLang]: `${description}`,
656
+ type: type || 'http://activitystrea.ms/schema/1.0/page'
657
+ }
658
+ },
659
+ objectType: objectType || 'Activity'
660
+ },
661
+ context: {
662
+ contextActivities: {}
663
+ }
664
+ }
665
+ //===================== contextActivity parent =====================//
666
+ /*
667
+ Define parent in the contextActivity
668
+ When:
669
+ 1- when we have the id (parent === module)
670
+ 2- when with have no id but we have a course_id (parent is cours_id)
671
+ */
672
+ const activityParent = (() => {
673
+ if (crsParams.courseID && !id)
674
+ return {
675
+ id: this.getConnectionInfo.activity_id.replace(
676
+ `/${crsParams.id}`,
677
+ ''
678
+ ),
679
+ objectType: objectType || 'Activity'
680
+ }
681
+ else if (id && id !== crsParams.courseID)
682
+ return {
683
+ id: `${this.getConnectionInfo.activity_id}`,
684
+ objectType: objectType || 'Activity'
685
+ }
686
+ else return null
687
+ })()
688
+
689
+ // Add the parent key of the context activity
690
+ if (activityParent)
691
+ stmt.context.contextActivities['parent'] = [activityParent]
692
+
693
+ //===================== contextActivity grouping =====================//
694
+
695
+ //Defining the Grouping of the context activity
696
+ const activityGrouping = (() => {
697
+ if (
698
+ activityParent &&
699
+ crsParams.courseID &&
700
+ activityParent.id.includes(crsParams.id)
701
+ )
702
+ return {
703
+ id: this.getConnectionInfo.activity_id.replace(
704
+ `/${crsParams.id}`,
705
+ ''
706
+ )
707
+ }
708
+ else return null
709
+ })()
710
+ //Adding the grouping of the context activity
711
+ if (activityGrouping)
712
+ stmt.context.contextActivities['grouping'] = [activityGrouping]
713
+
714
+ //add result data to statement
715
+ if (result) stmt['result'] = result
716
+
717
+ // Add duration info to statement when activity is complete
718
+ if (['completed', 'suspended', 'terminated'].includes(verb)) {
719
+ if (!stmt.result) stmt.result = {} // check if exist
720
+
721
+ let d
722
+
723
+ duration
724
+ ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
725
+ duration.split(':')[2]
726
+ }S`)
727
+ : (d = `PT${this.activityDuration.split(':')[0]}H${
728
+ this.activityDuration.split(':')[1]
729
+ }M${this.activityDuration.split(':')[2]}S`)
730
+
731
+ stmt.result['duration'] = d
732
+ //Set the completion status of the result
733
+ if (completion) stmt.result['completion'] = completion
734
+ }
735
+
736
+ // ===================== Extension of the Object definition =====================//
737
+ if (
738
+ extensions &&
739
+ extensions.constructor === Array &&
740
+ extensions.length > 0
741
+ ) {
742
+ //Validate each entry given in the extension Array
743
+ extensions.forEach((e) => {
744
+ //entry must be of type Object
745
+ if (e.constructor !== Object)
746
+ throw new Error(`'${e}' is not a valid value. Must be Object`)
747
+
748
+ //Entry Must have id and content keys
749
+ const validKey = ['id', 'content']
750
+ Object.keys(e).forEach((key) => {
751
+ if (!validKey.includes(key))
752
+ throw new Error(`Not valid key '${key}' for entry ${e}`)
753
+
754
+ //id must be a String
755
+ if (key === 'id' && e[key] && e[key].constructor !== String)
756
+ throw new Error(`'${key}' must be of type String`)
757
+ })
758
+
759
+ stmt.object.definition['extensions'] = {
760
+ ...stmt.object.definition['extensions'],
761
+ [`${this.getConnectionInfo.activity_id}/${e.id}`]: e.content
762
+ }
763
+ })
764
+ }
765
+
766
+ //============================================================
767
+ stmtsQueue.push(stmt)
768
+ })
769
+
770
+ this.$xapi._sendStatements(stmtsQueue, cb, withFetch)
771
+ }
772
+ },
773
+
774
+ /**
775
+ * @param {Function} cb
776
+ * @param {Bool} option - set if must use Fetch API or not. default = false
777
+ */
778
+ endLesson(cb, option = true) {
779
+ cb = cb || null
780
+ let text
781
+ //Defining the text to display for stmt description and definition
782
+ switch (this.$i18n.locale) {
783
+ case 'fr':
784
+ if (this.getModuleInfo.courseID)
785
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
786
+ else text = `Le ${this.getModuleInfo.id}`
787
+ break
788
+ case 'en':
789
+ if (this.getModuleInfo.courseID)
790
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
791
+ else text = `The ${this.getModuleInfo.id}`
792
+ break
793
+ }
794
+
795
+ // Retrieve only user data in lessons front its interaction that we want to send to the LRS.
796
+ // Note: User Settings are sent on a different URI and statement
797
+ const { isFistTime, userSettings, ...lessonsData } =
798
+ this.getUserInteraction
799
+
800
+ const stmt = {
801
+ verb: 'suspended',
802
+ definition: text,
803
+ description: text,
804
+ extensions: [
805
+ {
806
+ id: 'ending-point',
807
+ content: (() =>
808
+ this.$route.name == 'menu'
809
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
810
+ ._namedRoute
811
+ : this.$route.name)()
812
+ },
813
+ {
814
+ id: 'user-data',
815
+ content: {
816
+ routeHistory: (() => {
817
+ const history = this.getRouteHistory.toReversed() //get the route history from the last
818
+
819
+ history[0] = this.$route.meta // change the last recored route to the current route
820
+
821
+ return history.toReversed()
822
+ })(),
823
+ ...lessonsData
824
+ }
825
+ }
826
+ ],
827
+ duration: this.lessonDuration
828
+ }
829
+
830
+ const endStmt = { ...stmt }
831
+ endStmt.verb = 'terminated'
832
+ //================================STATEMENT FOR THE PLAYBAR ===============================================
833
+ //Creating custom statement
834
+ const stmtPlaybar = {
835
+ id: (() => {
836
+ if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
837
+ else return null
838
+ })(),
839
+ verb: 'played',
840
+ definition: text,
841
+ description: text,
842
+ extensions: [
843
+ {
844
+ id: 'playbar-values',
845
+ content: {
846
+ ...this.getMediaPlaybarValues()
847
+ }
848
+ }
849
+ ]
850
+ }
851
+ //================================STATEMENT FOR THE SETTINGS ===============================================
852
+
853
+ //Creating custom statement
854
+ const stmtUserSettings = {
855
+ id: (() => {
856
+ if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
857
+ else return null
858
+ })(),
859
+ verb: 'preferred',
860
+ definition: text,
861
+ description: text,
862
+ extensions: [
863
+ {
864
+ id: 'application-settings',
865
+ content: {
866
+ ...this.getApplicationSettings
867
+ }
868
+ // content: {
869
+ // bookmark: false
870
+ // } //for testing
871
+ }
872
+ ]
873
+ }
874
+ //================================END STATEMENT ===============================================
875
+
876
+ this.sendXapiStatements(
877
+ [stmtPlaybar, stmtUserSettings, stmt, endStmt],
878
+ null,
879
+ option
880
+ ) //send xapi statement
881
+
882
+ if (this.timerState === 'started') setTimeout(() => this.stopTimer(), 0) //clear the timer
883
+
884
+ if (cb) cb()
885
+ },
886
+
887
+ /** @description Method to reset the progression status of user
888
+ * the function reset first the user data in the app store then
889
+ * reset the idbStore if APP is running locally or prepare a statement with new userData Object to be sent over the LRS
890
+ * for the connected userAgent, activity if APP is Running online
891
+ * @event 'send-xapi-statement' emit to AppBaseModule.vue that will be executed by sendXapiStatement(s)
892
+ */
893
+ async resetUserData() {
894
+ this.$bus.$emit('set-comp-status', 'appBase', 'loading')
895
+ this.setUserMetaData({}) // resetting store record with existing for user data
896
+ this.setApplicationSettings({}) // resetting store record for settings
897
+ this.setRouteHistory([]) // resetting store record for all last visited pages
898
+
899
+ this.$bus.$emit('stop-timer')
900
+ if (this.getModuleInfo.packageType !== 'xapi') return
901
+
902
+ if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
903
+ return this.$idb.deleteDataInDB(this.getModuleInfo.idbID) //Must call the idb delete methode to reset indexDB store
904
+
905
+ //Send a completion statement for the current activity
906
+ let aTitle = `${this.$t('text.activity')} ${this.getConnectionInfo.activity_id}`
907
+
908
+ //Custom text for description and definition of the stament to be sent
909
+ let text
910
+ this.$i18n.locale == 'fr'
911
+ ? (text = `L'${aTitle} de ${this.getModuleInfo.id}`)
912
+ : (text = `The ${aTitle} of ${this.getModuleInfo.id}`)
913
+
914
+ /*Dispatch a send statement event to method AppBaseModule sendXapiStatement(s)
915
+ Content values are:
916
+ User data URI: "host_address/course_ID/Lesson_ID/user-data" ex: "http://localhost:8080/330N01FD6001/m1l1/ // user-data" and
917
+ User Bookmark URI: "host_address/course_ID/Lesson_ID/ending-point" ex: ""http://localhost:8080/330N01FD6001/m1l1/ending-point": "introduction"
918
+ */
919
+ const baseStmt = {
920
+ definition: text,
921
+ description: text
922
+ }
923
+ const exitStmt = {
924
+ ...baseStmt,
925
+ verb: 'exited'
926
+ }
927
+
928
+ const endStmt = {
929
+ ...baseStmt,
930
+ verb: 'terminated',
931
+ extensions: [
932
+ {
933
+ id: 'ending-point',
934
+ content: (() =>
935
+ this.$route.name == 'menu'
936
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
937
+ ._namedRoute
938
+ : this.$route.name)()
939
+ },
940
+ {
941
+ id: 'user-data',
942
+ content: new Object()
943
+ }
944
+ ],
945
+ duration: null,
946
+ completion: false // Resetting completion state value to
947
+ }
948
+ //================================STATEMENT FOR THE SETTINGS ===============================================
949
+
950
+ //Creating custom statement
951
+ const stmtUserSettings = {
952
+ ...baseStmt,
953
+ id: (() => {
954
+ if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
955
+ else return null
956
+ })(),
957
+
958
+ verb: 'preferred',
959
+ extensions: [
960
+ {
961
+ id: 'application-settings',
962
+ content: new Object()
963
+ }
964
+ ]
965
+ }
966
+ //================================END STATEMENT ===============================================
967
+
968
+ this.sendXapiStatements([stmtUserSettings, exitStmt, endStmt])
969
+
970
+ this.$bus.$emit('set-comp-status', 'appBase', 'ready')
971
+
972
+ // Creating a asynchronous fecth method that would resolve after a 2 second
973
+ const fetchData = async () => {
974
+ //funtion that will make a multiple request on the server
975
+ const requests = async () => {
976
+ const _url = new URL(this.getConnectionInfo.activity_id)
977
+ const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
978
+
979
+ const progress = await this.$xapi._getProgress(
980
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
981
+ this.getConnectionInfo.activity_id
982
+ )
983
+ const lessonPosition = await this.$xapi._getLessonPosition(
984
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
985
+ this.getConnectionInfo.activity_id
986
+ )
987
+ const applicationSettings = await this.$xapi._getPreferredSettings(
988
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
989
+ parentID
990
+ )
991
+ return { progress, lessonPosition, applicationSettings }
992
+ }
993
+ return new Promise((resolve) => {
994
+ setTimeout(() => resolve(requests()), 2000)
995
+ })
996
+ }
997
+
998
+ // Make request over user current data after sending statement to assure that the resetting worked
999
+ const usrData = await fetchData()
1000
+ this.$bus.$emit('reset-complete')
1001
+ console.warn('😄 USER STATUS: ', {
1002
+ User: this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
1003
+ ActivityID: this.getConnectionInfo.activity_id,
1004
+ Record: usrData
1005
+ })
1006
+ },
1007
+
1008
+ /**
1009
+ * @description- Scroll directly to the target* containt
1010
+ * target content can be any Node defined by the ID
1011
+ * @param {String} target = id of HTMLElement. if target don't existe will scroll to wrapper-content
1012
+ * @param {Obj} opt = scroll behavior to apply default { top: 0, left: 0, behavior: 'auto' }
1013
+ */
1014
+
1015
+ moveTo(target, opt = { top: 0, left: 0, behavior: 'auto' }) {
1016
+ if (target.constructor !== String)
1017
+ throw new Error(
1018
+ '⚠️ Not supported value for @target. Must be of type String'
1019
+ )
1020
+
1021
+ let skipTo = document.querySelector(`#wrapper-content`) //default definition of main element
1022
+
1023
+ if (target) {
1024
+ let targetEl = document.querySelector(`#${target}`) // search for node element specified as main
1025
+
1026
+ if (targetEl) skipTo = targetEl
1027
+ }
1028
+
1029
+ if (skipTo) opt.top = skipTo.offsetTop + opt.top // Set scroll top from target top
1030
+
1031
+ if (skipTo) skipTo.setAttribute('tabIndex', -1) //Allowing accessibility control with keyboard
1032
+ window.scrollTo(opt)
1033
+ if (skipTo) this.resetFocus(skipTo) //focus on the target
1034
+ },
1035
+
1036
+ /**
1037
+ * @description Reset the focus the element
1038
+ * @param {HTMLElement} e - element that will get focus
1039
+ */
1040
+ resetFocus(e) {
1041
+ if (e) e.focus()
1042
+ },
1043
+ /**
1044
+ * @description Mothods to validate that application settings are correctly
1045
+ * Opens error component when configurations are not correct
1046
+ *
1047
+ */
1048
+ checkForErrors() {
1049
+ let errMessage
1050
+
1051
+ if (validateAppContent(this.appConfig, true).length) {
1052
+ return (this.error = validateAppContent(this.appConfig, true))
1053
+ }
1054
+
1055
+ const { list: activitiesList } = this.getAllActivities()
1056
+ let { no_menu, is_single_activity } = this.appConfig
1057
+
1058
+ let noMenu = no_menu == undefined ? false : no_menu
1059
+
1060
+ let isSingleActivity =
1061
+ is_single_activity == undefined ? false : is_single_activity
1062
+
1063
+ let err
1064
+ if (this.getErrorMenu) err = true
1065
+ else err = false
1066
+
1067
+ let consoleMsg = ''
1068
+ //error if There is more than one activity when is_single_activity
1069
+ switch (true) {
1070
+ case isSingleActivity && activitiesList.size > 1:
1071
+ errMessage = `La configuration choisie ne permet pas d'avoir plus d'une activité dans l'application. \n Vous devez soit désactiver l'option 💲<b>is_single_activity</b> ou retirer TOUTES les autres activités`
1072
+
1073
+ consoleMsg = `Cannot have more than 1 activity with this settings configuration. Either disable 💲is_single_activity or DELETE ALL others activities`
1074
+ this.error.push(errMessage)
1075
+ break
1076
+
1077
+ case isSingleActivity && !noMenu:
1078
+ errMessage = `Le MENU n'est pas disponible avec la configuration choisie.\n Vous devez soit désactiver l'option 💲<b>no_menu</b> ou l'option 💲<b>is_single_activity</b>`
1079
+ consoleMsg = `Cannot have MENU with current settings configuration. Either set 💲no_menu:false or 💲is_single_activity:false`
1080
+ this.error.push(errMessage)
1081
+ break
1082
+
1083
+ case err:
1084
+ errMessage = `Il y une erreur dans votre fichier menu.setting.js. \n ouvrez votre console pour voir l'erreur et corriger la dans menu.setting.js `
1085
+ consoleMsg = this.getErrorMenu
1086
+ this.error.push(errMessage)
1087
+ break
1088
+ }
1089
+
1090
+ if (errMessage)
1091
+ return console.warn(
1092
+ `%c WARNING!>>> ${consoleMsg}`,
1093
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
1094
+ )
1095
+ }
1096
+ }
1097
+ }
1098
+ </script>
1099
+ <style lang="scss">
1100
+ #App-base {
1101
+ width: 100%;
1102
+ height: 100%;
1103
+ min-height: 100vh;
1104
+
1105
+ #overlay_loading {
1106
+ position: fixed !important;
1107
+ width: 100%;
1108
+ height: 100%;
1109
+ top: 0;
1110
+ left: 0;
1111
+ z-index: 9999;
1112
+ }
1113
+
1114
+ #build-info {
1115
+ opacity: 0.9;
1116
+ position: fixed;
1117
+ right: 0;
1118
+ bottom: 0;
1119
+ color: #fff;
1120
+ text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4);
1121
+ background-color: hotpink;
1122
+ font-family: serif, Impact, Arial;
1123
+ font-size: 11px;
1124
+ padding: 3px;
1125
+ cursor: pointer;
1126
+ z-index: 999;
1127
+ span {
1128
+ text-align: center;
1129
+ display: block;
1130
+ }
1131
+ }
1132
+
1133
+ .box {
1134
+ width: 100%;
1135
+ height: 100%;
1136
+ position: relative;
1137
+ }
1138
+ }
1139
+ .grp-spinners {
1140
+ transform: scale(1.5);
1141
+ #icon_loading {
1142
+ animation: spin 1.3s cubic-bezier(0.57, 0.87, 0.77, 0.87) infinite;
1143
+ }
1144
+ }
1145
+ @keyframes spin {
1146
+ 100% {
1147
+ transform: rotate(360deg);
1148
+ }
1149
+ }
1150
+ </style>