fcad-core-dragon 2.0.0-beta.1 → 2.0.0-beta.10

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 (163) hide show
  1. package/.editorconfig +6 -31
  2. package/.prettierrc +11 -0
  3. package/.vscode/extensions.json +8 -0
  4. package/.vscode/settings.json +16 -0
  5. package/CHANGELOG +153 -0
  6. package/README.md +28 -43
  7. package/documentation/.vitepress/config.js +114 -0
  8. package/documentation/api-examples.md +49 -0
  9. package/documentation/composants/app-base-button.md +58 -0
  10. package/documentation/composants/app-base-error-display.md +59 -0
  11. package/documentation/composants/app-base-popover.md +68 -0
  12. package/documentation/composants/app-comp-audio.md +75 -0
  13. package/documentation/composants/app-comp-branch-buttons.md +111 -0
  14. package/documentation/composants/app-comp-button-progress.md +53 -0
  15. package/documentation/composants/app-comp-carousel.md +53 -0
  16. package/documentation/composants/app-comp-container.md +53 -0
  17. package/documentation/composants/app-comp-input-checkbox-next.md +42 -0
  18. package/documentation/composants/app-comp-input-dropdown-next.md +34 -0
  19. package/documentation/composants/app-comp-input-radio-next.md +39 -0
  20. package/documentation/composants/app-comp-input-text-next.md +35 -0
  21. package/documentation/composants/app-comp-input-text-table-next.md +34 -0
  22. package/documentation/composants/app-comp-input-text-to-fill-dropdown-next.md +53 -0
  23. package/documentation/composants/app-comp-input-text-to-fill-next.md +31 -0
  24. package/documentation/composants/app-comp-jauge.md +31 -0
  25. package/documentation/composants/app-comp-menu-item.md +55 -0
  26. package/documentation/composants/app-comp-menu.md +29 -0
  27. package/documentation/composants/app-comp-navigation.md +41 -0
  28. package/documentation/composants/app-comp-note-call.md +53 -0
  29. package/documentation/composants/app-comp-note-credit.md +53 -0
  30. package/documentation/composants/app-comp-play-bar-next.md +53 -0
  31. package/documentation/composants/app-comp-pop-up-next.md +93 -0
  32. package/documentation/composants/app-comp-quiz-next.md +235 -0
  33. package/documentation/composants/app-comp-quiz-recall.md +53 -0
  34. package/documentation/composants/app-comp-svg-next.md +53 -0
  35. package/documentation/composants/app-comp-table-of-content.md +50 -0
  36. package/documentation/composants/app-comp-video-player.md +82 -0
  37. package/documentation/composants.md +46 -0
  38. package/documentation/composants_critiques/ModelPageComposant.md +53 -0
  39. package/documentation/composants_critiques/app-base-module.md +43 -0
  40. package/documentation/composants_critiques/app-base-page.md +48 -0
  41. package/documentation/composants_critiques/app-base.md +311 -0
  42. package/documentation/composants_critiques/main.md +15 -0
  43. package/documentation/demarrage.md +50 -0
  44. package/documentation/deploiement.md +58 -0
  45. package/documentation/index.md +33 -0
  46. package/documentation/markdown-examples.md +85 -0
  47. package/documentation/public/npm_version.png +0 -0
  48. package/documentation/public/vite.svg +15 -0
  49. package/documentation/public/vuejs.svg +2 -0
  50. package/documentation/public/vuetify.svg +6 -0
  51. package/eslint.config.js +60 -0
  52. package/package.json +43 -47
  53. package/src/$locales/en.json +86 -108
  54. package/src/$locales/fr.json +66 -127
  55. package/src/assets/data/onboardingMessages.json +1 -1
  56. package/src/components/AppBase.vue +960 -405
  57. package/src/components/AppBaseButton.test.js +21 -0
  58. package/src/components/AppBaseButton.vue +42 -10
  59. package/src/components/AppBaseErrorDisplay.vue +207 -189
  60. package/src/components/AppBaseFlipCard.vue +1 -0
  61. package/src/components/AppBaseModule.vue +769 -977
  62. package/src/components/AppBasePage.vue +635 -81
  63. package/src/components/AppBasePopover.vue +41 -0
  64. package/src/components/AppBaseSkeleton.vue +66 -0
  65. package/src/components/AppCompAudio.vue +256 -0
  66. package/src/components/AppCompBranchButtons.vue +79 -153
  67. package/src/components/AppCompButtonProgress.vue +21 -36
  68. package/src/components/AppCompCarousel.vue +231 -87
  69. package/src/components/{AppCompTranscript.vue → AppCompContainer.vue} +12 -2
  70. package/src/components/AppCompInputCheckBoxNx.vue +323 -0
  71. package/src/components/AppCompInputDropdownNx.vue +299 -0
  72. package/src/components/AppCompInputRadioNx.vue +284 -0
  73. package/src/components/AppCompInputTextNx.vue +153 -0
  74. package/src/components/AppCompInputTextTableNx.vue +202 -0
  75. package/src/components/AppCompInputTextToFillDropdownNx.vue +340 -0
  76. package/src/components/AppCompInputTextToFillNx.vue +313 -0
  77. package/src/components/AppCompJauge.vue +36 -10
  78. package/src/components/AppCompMenu.vue +246 -32
  79. package/src/components/AppCompMenuItem.vue +87 -21
  80. package/src/components/AppCompNavigation.vue +470 -447
  81. package/src/components/AppCompNoteCall.vue +93 -58
  82. package/src/components/AppCompNoteCredit.vue +423 -96
  83. package/src/components/AppCompPlayBarNext.vue +2288 -0
  84. package/src/components/AppCompPopUpNext.vue +504 -0
  85. package/src/components/AppCompQuizNext.vue +510 -0
  86. package/src/components/AppCompQuizRecall.vue +199 -99
  87. package/src/components/AppCompSVGNext.vue +346 -0
  88. package/src/components/AppCompSettingsMenu.vue +17 -16
  89. package/src/components/AppCompTableOfContent.vue +262 -99
  90. package/src/components/AppCompVideoPlayer.vue +183 -142
  91. package/src/components/BaseModule.vue +8 -20
  92. package/src/components/tests__/AppBaseButton.spec.js +53 -0
  93. package/src/components/tests__/useTimer.spec.js +91 -0
  94. package/src/composables/useIdleDetector.js +56 -0
  95. package/src/composables/useQuiz.js +89 -0
  96. package/src/composables/useTimer.js +172 -0
  97. package/src/directives/nvdaFix.js +53 -0
  98. package/src/externalComps/ModuleView.vue +22 -0
  99. package/src/externalComps/SummaryView.vue +91 -0
  100. package/src/main.js +397 -148
  101. package/src/module/stores/appStore.js +947 -0
  102. package/src/module/xapi/ADL.js +241 -60
  103. package/src/module/xapi/Crypto/Hasher.js +8 -8
  104. package/src/module/xapi/Crypto/WordArray.js +6 -6
  105. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +4 -4
  106. package/src/module/xapi/Crypto/algorithms/C_algo.js +14 -18
  107. package/src/module/xapi/Crypto/algorithms/HMAC.js +1 -1
  108. package/src/module/xapi/Crypto/algorithms/SHA1.js +1 -1
  109. package/src/module/xapi/Crypto/encoders/Base.js +7 -7
  110. package/src/module/xapi/Crypto/encoders/Base64.js +3 -3
  111. package/src/module/xapi/Crypto/encoders/Hex.js +2 -2
  112. package/src/module/xapi/Crypto/encoders/Latin1.js +3 -3
  113. package/src/module/xapi/Crypto/encoders/Utf8.js +3 -3
  114. package/src/module/xapi/Statement/index.js +3 -3
  115. package/src/module/xapi/launch.js +10 -10
  116. package/src/module/xapi/utils.js +17 -17
  117. package/src/module/xapi/wrapper.js +219 -214
  118. package/src/module/xapi/xapiStatement.js +29 -29
  119. package/src/plugins/analytics.js +34 -0
  120. package/src/plugins/bus.js +7 -2
  121. package/src/plugins/gsap.js +5 -7
  122. package/src/plugins/helper.js +97 -34
  123. package/src/plugins/i18n.js +13 -18
  124. package/src/plugins/idb.js +45 -30
  125. package/src/plugins/save.js +1 -1
  126. package/src/plugins/scorm.js +15 -15
  127. package/src/plugins/xapi.js +2 -2
  128. package/src/public/index.html +22 -10
  129. package/src/router/index.js +29 -13
  130. package/src/router/routes.js +29 -54
  131. package/src/shared/generalfuncs.js +186 -30
  132. package/src/shared/validators.js +809 -40
  133. package/vitest.config.js +19 -0
  134. package/.eslintignore +0 -29
  135. package/.eslintrc.js +0 -86
  136. package/.prettierrc.js +0 -5
  137. package/babel.config.js +0 -3
  138. package/src/components/AppBaseDragChoice.vue +0 -91
  139. package/src/components/AppBaseDropZone.vue +0 -112
  140. package/src/components/AppCompBif.vue +0 -120
  141. package/src/components/AppCompDragAndDrop.vue +0 -339
  142. package/src/components/AppCompInputAssociation.vue +0 -332
  143. package/src/components/AppCompInputCheckBox.vue +0 -227
  144. package/src/components/AppCompInputDropdown.vue +0 -184
  145. package/src/components/AppCompInputRadio.vue +0 -169
  146. package/src/components/AppCompInputTextBox.vue +0 -91
  147. package/src/components/AppCompInputTextTable.vue +0 -155
  148. package/src/components/AppCompInputTextToFillDropdown.vue +0 -255
  149. package/src/components/AppCompInputTextToFillText.vue +0 -164
  150. package/src/components/AppCompMediaPlayer.vue +0 -397
  151. package/src/components/AppCompPlayBar.vue +0 -1319
  152. package/src/components/AppCompPopUp.vue +0 -522
  153. package/src/components/AppCompPopover.vue +0 -27
  154. package/src/components/AppCompQuiz.vue +0 -2989
  155. package/src/components/AppCompSVG.vue +0 -309
  156. package/src/mixins/$pageMixins.js +0 -459
  157. package/src/mixins/$quizMixins.js +0 -456
  158. package/src/mixins/timerMixin.js +0 -156
  159. package/src/module/store.js +0 -895
  160. package/src/plugins/timeManager.js +0 -77
  161. package/src/routes_bckp.js +0 -313
  162. package/src/routes_static.js +0 -344
  163. package/vue.config.js +0 -83
@@ -1,40 +1,101 @@
1
1
  <!--
2
2
  @ Description: This is a root component to create the App
3
- @ What it does: Many things
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>
4
8
  @ Must be used.
5
9
  -->
6
10
  <template>
7
11
  <div id="App-base" fluid :class="{ iPad: resizeiPad }">
8
- <transition name="bounce" mode="in-out">
9
- <div
10
- v-if="showBuildInfo && !buildInfoClicked"
11
- id="build-info"
12
- @click="buildInfoClicked = true"
13
- >
14
- <span>{{ getModuleInfo.courseID.toUpperCase() }}</span>
15
- <span>FCAD {{ this.$helper.getFcadVersion() }}</span>
16
- <span>{{ this.$helper.getBuildTime() }}</span>
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
+ aria-hidden="true"
29
+ @click="buildInfoClicked = true"
30
+ >
31
+ <span>{{ getModuleInfo.courseID.toUpperCase() }}</span>
32
+ <span>FCAD {{ $helper.getFcadVersion() }}</span>
33
+ <span>template {{ $helper.getTemplateVersion() }}</span>
34
+ <span>{{ $helper.getBuildTime() }}</span>
35
+ </div>
36
+ </transition>
37
+
38
+ <router-view class="box" />
39
+ <div v-if="appDebugMode" class="timer">
40
+ <!-- <div class="timer"> -->
41
+ <div>Act: {{ appTimer.ISOTimeParser(activityDuration) }}</div>
42
+ <span>&nbsp;|&nbsp;</span>
43
+ <div>Les: {{ appTimer.ISOTimeParser(lessonDuration) }}</div>
17
44
  </div>
18
- </transition>
45
+ <app-icons-next :extra-icons="userExtraIcons" />
46
+ </template>
47
+
48
+ <v-overlay
49
+ id="overlay_loading"
50
+ :model-value="!appReady && initialLoading"
51
+ class="align-center justify-center"
52
+ scrim="white"
53
+ opacity="0.9"
54
+ :persistent="true"
55
+ >
56
+ <div class="text-center grp-spinners">
57
+ <v-icon id="icon_loading" icon="mdi-dots-circle" size="x-large" />
19
58
 
20
- <router-view class="box" />
21
- <app-icons />
59
+ <span class="sr-only">
60
+ {{ $t('message.loading_state_msg') }}
61
+ </span>
62
+ </div>
63
+ </v-overlay>
22
64
  </div>
23
65
  </template>
24
66
  <script>
25
- import { mapGetters } from 'vuex'
67
+ import { mapState, mapActions } from 'pinia'
68
+ import { useAppStore } from '../module/stores/appStore.js'
69
+ import { Timer } from '../composables/useTimer.js'
70
+ import { IdleDetector } from '../composables/useIdleDetector.js'
71
+ import { validateAppContent } from '../shared/validators'
72
+ import { version as fcadVersion } from '../../package.json'
73
+ import { computed, reactive } from 'vue'
74
+ import mobileDetect from 'mobile-detect'
26
75
 
27
76
  export default {
77
+ provide() {
78
+ return {
79
+ lessonDuration: computed(() => this.lessonDuration), //exposing the lesson Duration variable to all components
80
+ elapsedIdleTime: computed(() => this.idleDetector.getElapsedTime()), //exposing idle detector elapse time to all components
81
+ appTimer: this.appTimer //exposing the app timer instance to all components
82
+ }
83
+ },
28
84
  props: {
29
85
  appConfig: {
30
86
  type: Object,
31
- default: () => {
32
- return {
33
- lang: 'fr' // set language of App: default French
34
- }
87
+ required: true,
88
+ validator: (value) => {
89
+ if (import.meta.env.DEV) return validateAppContent(value).length === 0
35
90
  }
36
91
  }
37
92
  },
93
+ setup() {
94
+ const store = useAppStore()
95
+ const appTimer = reactive(new Timer('ativityTimer')) // Making Timer instance reactive to be able to track changes
96
+ const idleDetector = reactive(new IdleDetector()) // Making detector instance reactive.
97
+ return { store, appTimer, idleDetector }
98
+ },
38
99
 
39
100
  data() {
40
101
  return {
@@ -43,137 +104,279 @@ export default {
43
104
  initialDeviceOrentation: null,
44
105
  appIsFullScreen: false,
45
106
  resizeiPad: false,
46
- buildInfoClicked: false
107
+ buildInfoClicked: false,
108
+ error: [],
109
+ initialLoading: true,
110
+ f5KeyPressed: false,
111
+ lessonDuration: 0,
112
+ idleTimeout: 5 * 60000, //5 mins: this is the time of inactivity before considering the user is idle
113
+ starterTimeout: null
47
114
  }
48
115
  },
49
116
  computed: {
50
- ...mapGetters([
117
+ ...mapState(useAppStore, [
51
118
  'getCurrentBrowser',
52
119
  'getIsMobile',
53
120
  'getDeviceType',
54
121
  'getModuleInfo',
122
+ 'getAppStatus',
55
123
  'getUserInteraction',
56
- 'getConnectionInfo'
124
+ 'getConnectionInfo',
125
+ 'getAllActivities',
126
+ 'getMediaPlaybarValues',
127
+ 'getAppConfigs',
128
+ 'getErrorMenu',
129
+ 'getRouteHistory',
130
+ 'getBookmarkEnabled',
131
+ 'getAppDebugMode',
132
+ 'getApplicationSettings'
57
133
  ]),
134
+ appDebugMode() {
135
+ return this.getAppDebugMode
136
+ },
137
+ activityDuration() {
138
+ return this.appTimer.getTime() // return the time duration in seconds
139
+ },
140
+
58
141
  getwidth() {
59
142
  return window.innerWidth
60
143
  },
61
144
  getheight() {
62
145
  return window.innerHeight
63
146
  },
147
+
64
148
  showBuildInfo() {
65
149
  return (
66
- process.env.NODE_ENV === 'production' &&
150
+ import.meta.env.PROD &&
67
151
  window.location.host === 'projets.cegepadistance.ca'
68
152
  )
153
+ },
154
+ appReady() {
155
+ let readyState = this.getAppStatus === 'ready' ? true : false
156
+ return readyState
157
+ },
158
+
159
+ displayLang() {
160
+ let lang = false
161
+ const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
162
+
163
+ if (this.getModuleInfo.packageType === 'xapi') {
164
+ lang = displayList.find((l) => l.includes(this.$i18n.locale))
165
+ }
166
+ return lang
167
+ },
168
+ userExtraIcons() {
169
+ const icons = this.$helper.getSettingsFromStore('extra_icons')
170
+ ? this.$helper.getSettingsFromStore('extra_icons')
171
+ : null
172
+ return icons
173
+ } /**
174
+ * @description Set default value for bookmark
175
+ */,
176
+ bookmarkActive() {
177
+ return this.getBookmarkEnabled
69
178
  }
70
179
  },
71
180
  watch: {
72
- getConnectionInfo: {
73
- //in development environment (localhost), don't wait for the axios call
74
-
75
- immediate: process.env.NODE_ENV === 'development',
76
- handler() {
77
- /**
78
- * Attach listener to detect when user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile,
79
- * switches from the browser to a different app To save user data to LRS/LMS.
80
- * As of 06/2021 MDN Web Doc advise preferring the 'visibilitychange' event over 'unload/beforeunload'
81
- * event to send data over a serveur.
82
- * Ref:https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
83
- * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
84
- */
85
-
86
- if (this.getIsMobile) {
87
- document.addEventListener(
88
- 'visibilitychange',
89
- () => {
90
- if (document.visibilityState === 'hidden') {
91
- this.executeCloseEventTriggered()
92
- }
93
- },
94
- false
95
- )
96
- } else {
97
- //attached listener to terminate LMS|LRS communication
98
- if (
99
- this.getModuleInfo.packageType === 'xapi' &&
100
- this.getConnectionInfo &&
101
- this.getConnectionInfo.actor &&
102
- this.getConnectionInfo.remote
103
- ) {
104
- window.addEventListener('beforeunload', (e) => {
105
- this.executeCloseEventTriggered()
106
- })
107
- } else
108
- window.addEventListener(
109
- 'beforeunload',
110
- this.executeCloseEventTriggered
111
- )
181
+ initialLoading: {
182
+ deep: true,
183
+ handler(newVal) {
184
+ if (newVal !== false) {
185
+ return
112
186
  }
113
- // initialize scorm Commuinication with LMS || xAPI LRS
114
- if (this.getModuleInfo.packageType === 'scorm') this.$scorm.Initialize()
115
- else if (
116
- this.getModuleInfo.packageType === 'xapi' &&
117
- this.getConnectionInfo &&
118
- this.getConnectionInfo.actor &&
119
- this.getConnectionInfo.remote
120
- ) {
121
- const {
122
- auth,
123
- endpoint,
124
- registration = this.$xapi.ruuid()
125
- } = this.getConnectionInfo
126
-
127
- const config = {
128
- auth,
129
- endpoint,
130
- registration,
131
- activity_platform: `SIPI_organizationId`
132
- }
133
-
134
- this.$xapi._configLRS(config) // configure and launch LRS
135
- this.fetchSettingsFromServer()
187
+ const eventParams = {
188
+ fcad_version: fcadVersion,
189
+ course_id: this.getAppConfigs.crs_id,
190
+ lesson_id: this.getAppConfigs.id
191
+ }
192
+ this.$analytics.sendEvent('user_meta', eventParams)
193
+ }
194
+ },
195
+ 'store.$state.userDataLoaded': {
196
+ handler(newValue) {
197
+ if (!newValue) return this.updateTracker('appBase', 'loading')
198
+ this.updateTracker('appBase', 'ready')
199
+ },
200
+ immediate: true
201
+ },
202
+ appReady: {
203
+ handler(newValue) {
204
+ if (newValue) {
205
+ this.setInitialLoadingDone()
136
206
  }
137
207
  }
138
208
  }
139
209
  },
140
210
 
141
211
  created() {
142
- this.setProgress()
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.initializeApp(this.appConfig)
220
+ }
221
+
143
222
  window.versionFCAD = this.$helper.getFcadVersionString()
144
223
  //check if this is running in a mobile environment and register state in the store
145
- const mobileDetect = require('mobile-detect')
146
-
147
224
  const md = new mobileDetect(window.navigator.userAgent)
148
- md.mobile()
149
- ? this.$store.dispatch('setMobileState', true)
150
- : this.$store.dispatch('setMobileState', false)
225
+ md.mobile() !== null
226
+ ? this.setMobileState(true)
227
+ : this.setMobileState(false)
151
228
  const currentBrowser = this.getBrowser() //get current browser vendor
152
229
 
153
230
  // register the current browser in the store
154
- this.$store.dispatch('setCurrentBrowser', currentBrowser)
231
+ this.setCurrentBrowser(currentBrowser)
155
232
 
156
233
  // register device type running the App in the store (ios, Android or Deskop)
157
- this.$store.dispatch('setDeviceType', this.detectDevice())
158
- this.fetchSettingsFromServer()
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
+ this.$analytics.init(this.getAppConfigs.analytics_id, this.$router)
243
+ this.$bus.$on('start-timer', this.startAppTimer)
244
+ this.$bus.$on('stop-app-timer', this.stopAppTimer)
245
+ this.$bus.$on('start-idle-detector', this.setIdleDetector)
246
+ this.$bus.$on('stop-idle-detector', this.unsetIdleDetector)
159
247
  },
160
- mounted() {
161
- // set the language of the app
162
- this.setLocale(this.appConfig.lang)
163
- this.$bus.$on('set-comp-status', (name, status) =>
164
- this.updateTracker(name, status)
248
+ beforeMount() {
249
+ window.addEventListener(
250
+ 'beforeunload',
251
+ (e) => {
252
+ this.executeCloseEventTriggered()
253
+ // e.preventDefault()
254
+ // e.returnValue = true
255
+ },
256
+ true
165
257
  )
258
+ },
166
259
 
167
- this.$bus.$on('set-user-progress', this.setProgress())
260
+ mounted() {
261
+ // set the language of the app
262
+ this.setLocale(this.getAppConfigs.lang)
263
+ window.addEventListener('keydown', this.handleF5KeyPressed)
264
+ window.addEventListener('beforeunload', this.handleBeforeUnload)
168
265
  },
169
- beforeDestroy() {
170
- this.$bus.$off('set-comp-status')
266
+ beforeUnmount() {
267
+ this.$bus.$off('set-comp-status', this.updateTracker)
268
+ this.$bus.$off('reset-userdata', this.resetUserData)
269
+ this.$bus.$off('fire-exit-event', this.endLesson)
270
+ this.$bus.$off('reset-focus-on', this.resetFocus)
271
+ this.$bus.$off('send-xapi-statement', this.sendXapiStatements)
272
+ this.$bus.$off('move-to-target', this.moveTo)
273
+ this.$bus.$off('start-idle-detector', this.setIdleDetector)
274
+ this.$bus.$off('stop-idle-detector', this.unsetIdleDetector)
275
+ this.$bus.$off('start-timer', this.startAppTimer)
276
+ this.$bus.$off('stop-app-timer', this.stopAppTimer)
277
+ window.removeEventListener('keydown', this.handleF5KeyPressed)
278
+ window.removeEventListener('beforeunload', this.handleBeforeUnload)
279
+ if (this.getAppConfigs.remote && this.unsubscribeToSetConfig)
280
+ this.unsubscribeToSetConfig() //stop watching changing in store to save to local storage
171
281
  },
172
282
  methods: {
283
+ ...mapActions(useAppStore, [
284
+ 'setDeviceType',
285
+ 'updateDataFetchFromServer',
286
+ 'setUserMetaData',
287
+ 'setRouteHistory',
288
+ 'setApplicationSettings',
289
+ 'setMediaPlaybarValues',
290
+ 'updateCompStatusTracker',
291
+ 'initializeApp',
292
+ 'setMobileState',
293
+ 'setCurrentBrowser'
294
+ ]),
295
+ /**
296
+ * @description start the app timer instance
297
+ * Check if the timer is already started, if yes it add the activity duration
298
+ * to the lesson duration and restart the timer
299
+ */
300
+
301
+ startAppTimer() {
302
+ if (this.$route.name === 'module') return // don't start timer when in menu
303
+ if (this.appTimer.getTimerState() == 'started') {
304
+ this.lessonDuration += this.activityDuration //
305
+ this.appTimer.stop() // Start the activity timer
306
+ }
307
+
308
+ //Delay actually one second before starting timers
309
+ this.starterTimeout = setTimeout(() => {
310
+ this.appTimer.start() // Start the activity timer
311
+ this.setIdleDetector() //start the idle time detector
312
+ }, 500)
313
+ },
314
+
315
+ /**
316
+ * @description stop the app timer instance
317
+ * The function add the activity duration to the lesson duration and stop the timer
318
+ */
319
+ stopAppTimer() {
320
+ this.lessonDuration += this.activityDuration
321
+ this.appTimer.stop() // stop timer
322
+ clearTimeout(this.starterTimeout)
323
+ },
324
+
325
+ /**
326
+ * @description set the idle detector to start detecting idle time
327
+ * Attach DOM event listeners to reset the idle timer
328
+ * The idle detector will start after 1 second after the app is loaded
329
+ * The idle detector will pause the app timer when idle time is reached
330
+ */
331
+
332
+ setIdleDetector() {
333
+ setTimeout(() => {
334
+ this.idleDetector.startIdleTimer(
335
+ () => this.appTimer.pause(),
336
+ this.idleTimeout
337
+ )
338
+ document.addEventListener('mousedown', this.resetIdleTimer, false) //set eventlisteners
339
+ document.addEventListener('mousemove', this.resetIdleTimer, false) //set eventlisteners
340
+ document.addEventListener('keypress', this.resetIdleTimer, false) //set eventlisteners
341
+ document.addEventListener('touchmove', this.resetIdleTimer, false) //set eventlisteners
342
+ }, 800)
343
+ },
344
+ /**
345
+ * @description unset the idle detector
346
+ * stop the idle timer and remove DOM event to stop listening the idle timer
347
+ */
348
+ unsetIdleDetector() {
349
+ this.idleDetector.stopIdleTimer()
350
+ document.removeEventListener('mousedown', this.resetIdleTimer, false)
351
+ document.removeEventListener('mousemove', this.resetIdleTimer, false)
352
+ document.removeEventListener('keypress', this.resetIdleTimer, false)
353
+ document.removeEventListener('touchmove', this.resetIdleTimer, false)
354
+ },
355
+ /**
356
+ * @description reset the idle timer
357
+ * The function stop the idle timer and restart it
358
+ * start also the app timer if it was paused
359
+ *
360
+ */
361
+ resetIdleTimer() {
362
+ if (this.$route.name == 'menu' || this.$route.name == 'module') return // don't reset idle timer when in menu
363
+ this.idleDetector.stopIdleTimer()
364
+
365
+ if (this.appTimer.getTimerState() == 'stopped') this.appTimer.start()
366
+ this.idleDetector.startIdleTimer(
367
+ () => this.appTimer.pause(),
368
+ this.idleTimeout
369
+ )
370
+ },
371
+
372
+ setInitialLoadingDone() {
373
+ this.initialLoading = false
374
+ },
173
375
  /**
174
376
  * @description set the desired language for the app default is french
175
377
  * @param {String} [lang=fr]
176
378
  */
379
+
177
380
  setLocale(lang) {
178
381
  if (!lang) lang = 'fr'
179
382
  else {
@@ -183,11 +386,15 @@ export default {
183
386
  else if (lang === 'english' || lang === 'anglais') lang = 'en'
184
387
  else lang = lang.substring(0, 2).toLowerCase()
185
388
  this.$i18n.locale = lang
389
+ //Set vuetify locale
390
+ this.$vuetify.locale.current = lang
186
391
  }
187
392
  },
393
+
188
394
  getScormState() {
189
395
  return this.$scorm.GetValue('cmi.suspend_data')
190
396
  },
397
+
191
398
  getBrowser() {
192
399
  let browser
193
400
  const test = (regexp) => regexp.test(window.navigator.userAgent) //defining the testing fonction
@@ -241,50 +448,12 @@ export default {
241
448
  return device
242
449
  },
243
450
 
244
- fetchSettingsFromServer() {
245
- if (this.getModuleInfo.packageType !== 'xapi') return
246
- if (!this.getConnectionInfo || !this.getConnectionInfo.remote) return
247
-
248
- const activity_id = this.getModuleInfo.courseID
249
- ? this.getConnectionInfo.activity_id.replace(
250
- `/${this.getModuleInfo.id}`,
251
- ''
252
- )
253
- : this.getConnectionInfo.activity_id
254
-
255
- //Get existing record for user preferred settings
256
- const applicationSettings = this.$xapi._getPreferredSettings(
257
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
258
- activity_id
259
- )
260
-
261
- if (applicationSettings.length && applicationSettings[0].userSettings)
262
- //save result to store
263
- this.$store.dispatch(
264
- 'setApplicationSettings',
265
- applicationSettings[0].userSettings
266
- )
267
-
268
- //Get existing record for play bar settings
269
- const pbValues = this.$xapi._getPlaybarValues(
270
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
271
- activity_id
272
- )
273
-
274
- if (pbValues.length) {
275
- //save result to store
276
- this.$store.dispatch('setMediaVolume', pbValues[0].volume)
277
- this.$store.dispatch('setMediaSubtitles', pbValues[0].subtitlesEnabled)
278
- }
279
- },
280
-
281
451
  executeCloseEventTriggered() {
282
452
  if (
283
453
  this.getModuleInfo.packageType === 'scorm' &&
284
454
  this.$scorm.initialized
285
455
  ) {
286
- this.$bus.$emit('save-to-scorm') // emit event to save to scorm before closing the app
287
- this.$scorm.Finish()
456
+ this.$bus.$emit('save-to-scorm', 'disconnect') // emit event to save to scorm before closing the app
288
457
  }
289
458
  //Xapi context
290
459
  else if (
@@ -293,111 +462,639 @@ export default {
293
462
  this.getConnectionInfo.actor &&
294
463
  this.getConnectionInfo.remote
295
464
  ) {
296
- // Communicate ending of activity
297
- this.$bus.$emit('fire-exit-event', null, true)
298
- this.routeChangeCounter = 0 //reset counter after saving
465
+ this.endLesson(null, true)
299
466
  }
300
467
  },
301
- setProgress() {
302
- this.updateTracker('appBase', 'loading')
303
- const packageType = this.getModuleInfo.packageType
304
-
305
- switch (packageType) {
306
- case 'scorm':
307
- // Get saved data from suspend_data or from localStorage to update the store:
308
- // LMS is connected
309
- if (this.$scorm.initialized) {
310
- // chacked if there is a existing record in the LMS
311
- if (this.$scorm.GetValue('cmi.suspend_data') !== '') {
312
- const scormRecord = this.$scorm
313
- .GetValue('cmi.suspend_data')
314
- .replace(/\\/g, '')
315
- const userProgress = JSON.parse(scormRecord).userData
316
- const routeHistory = JSON.parse(scormRecord).routeHistory
317
-
318
- this.$store.dispatch('setUserMetaData', userProgress)
319
- this.$store.dispatch('setRouteHistory', routeHistory) // update store recored with existing record
320
- this.updateTracker('appBase', 'ready')
321
- }
322
468
 
323
- // No LMS use LocalStorage record
324
- } else {
325
- this.$idb.openDB().then(() => {
326
- this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
327
- if (res && res.$record) {
328
- const {
329
- routeHistory,
330
- userSettings,
331
- ...userData
332
- } = res.$record
333
- this.$store.dispatch('setUserMetaData', userData) // update store record with existing record
334
- this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing route info
335
- if (userSettings)
336
- this.$store.dispatch('setApplicationSettings', userSettings) // update store record with existing user settings
337
- this.updateTracker('appBase', 'ready')
469
+ updateTracker(name, status) {
470
+ this.updateCompStatusTracker({ name, status })
471
+ },
472
+ //============================Multiple Satements Sending at once======================================
473
+ /**
474
+ * @description Send a custom statements to the lrs. Receive some custom params and build a statement to send
475
+ * @param {Array} stmts- array of stamtents objects that will be send to the server
476
+ * @param {Function} cb
477
+ */
478
+ async sendXapiStatements(stmts, cb = null, withFetch = true) {
479
+ cb = cb || null
480
+ if (!stmts) return
481
+ let stmtsArray = []
482
+
483
+ stmts.constructor === Object
484
+ ? stmtsArray.push(stmts)
485
+ : (stmtsArray = [...stmts])
486
+
487
+ const crsParams = this.getModuleInfo
488
+
489
+ if (
490
+ this.getConnectionInfo &&
491
+ this.getConnectionInfo.actor &&
492
+ this.getConnectionInfo.remote
493
+ ) {
494
+ const stmtsQueue = []
495
+
496
+ stmtsArray.forEach((stmtObj) => {
497
+ const {
498
+ id,
499
+ result = null,
500
+ definition,
501
+ objectType,
502
+ type,
503
+ description,
504
+ verb,
505
+ extensions,
506
+ duration,
507
+ completion
508
+ } = stmtObj
509
+
510
+ //define the activity id
511
+ let activityId
512
+ //=========================== activity ID of Object ==========================================
513
+ /*
514
+ * Define the acitivity id of the stmt according to following:
515
+ * The statement is sent at component/ element level : there is Id and Id is not null (id of the element)
516
+ * The statement is sent at module level. there is no id
517
+ * the Statement must be sent at the course level: There course_id exist and id is the course_id
518
+ */
519
+ if (id && id !== crsParams.courseID)
520
+ activityId = `${this.getConnectionInfo.activity_id}/${id}`
521
+ else if (crsParams.courseID && id === crsParams.courseID)
522
+ activityId = this.getConnectionInfo.activity_id.replace(
523
+ `/${crsParams.id}`,
524
+ ''
525
+ )
526
+ else activityId = `${this.getConnectionInfo.activity_id}`
527
+
528
+ //define the statement object
529
+ let stmt = {
530
+ actor: this.getConnectionInfo.actor,
531
+ verb: (() => {
532
+ if (verb && this.$xapi.verbs[verb.trim()])
533
+ return this.$xapi.verbs[verb.trim()]
534
+ else {
535
+ /**
536
+ * Determine the verb to use by:
537
+ * Checking if the activity is already included in the data fetch from server.
538
+ * If not, fetch directly from the server
539
+ * There is a found, verb is 'RESUMED'
540
+ * There is no found, verb is 'INITIALIZED'
541
+ */
542
+
543
+ //Regex to test that id contains list of word
544
+ const regex =
545
+ /(menu|activite_(\d)*|A(\d){2}|conclusion|introduction)$/gm
546
+
547
+ switch (true) {
548
+ case regex.test(activityId): {
549
+ //ID is of activity (menu, conclusion, activite, intro )
550
+ const activityStr = activityId.split('/').toReversed()[0]
551
+
552
+ if (!this.getUserInteraction[activityId])
553
+ return this.$xapi.verbs.initialized
554
+
555
+ let activity_ref
556
+ //Define the activity reference
557
+ if (['menu', 'introduction', 'A00'].includes(activityStr))
558
+ activity_ref = 'A00'
559
+ else if (['conclusion', 'A99'].includes(activityStr))
560
+ activity_ref = 'A99'
561
+ else if (activityStr.includes('activite_')) {
562
+ const aNum = activityStr.split('_')[1]
563
+ activity_ref = aNum.length > 1 ? `A${aNum}` : `A0${aNum}`
564
+ } else activity_ref = activityStr // should be AXX
565
+
566
+ if (
567
+ this.getUserInteraction &&
568
+ this.getUserInteraction[activity_ref]
569
+ )
570
+ return this.$xapi.verbs.resumed
571
+ else return this.$xapi.verbs.initialized
572
+ }
573
+ default: {
574
+ //ID of Lesson or doesn't exist relate (menu, conclusion, activite, intro )
575
+ if (
576
+ this.$xapi._getAgent(
577
+ this.getConnectionInfo.actor.mbox.replace(
578
+ 'mailto:',
579
+ ''
580
+ ),
581
+ activityId
582
+ )
583
+ )
584
+ return this.$xapi.verbs.resumed
585
+ else return this.$xapi.verbs.initialized
586
+ }
587
+ }
588
+ }
589
+ })(),
590
+ object: {
591
+ id: activityId,
592
+ definition: {
593
+ name: {
594
+ [this.displayLang]: `${definition}`
595
+ },
596
+ description: {
597
+ [this.displayLang]: `${description}`,
598
+ type: type || 'http://activitystrea.ms/schema/1.0/page'
338
599
  }
600
+ },
601
+ objectType: objectType || 'Activity'
602
+ },
603
+ context: {
604
+ contextActivities: {}
605
+ }
606
+ }
607
+ //===================== contextActivity parent =====================//
608
+ /*
609
+ Define parent in the contextActivity
610
+ When:
611
+ 1- when we have the id (parent === module)
612
+ 2- when with have no id but we have a course_id (parent is cours_id)
613
+ */
614
+ const activityParent = (() => {
615
+ if (crsParams.courseID && !id)
616
+ return {
617
+ id: this.getConnectionInfo.activity_id.replace(
618
+ `/${crsParams.id}`,
619
+ ''
620
+ ),
621
+ objectType: objectType || 'Activity'
622
+ }
623
+ else if (id && id !== crsParams.courseID)
624
+ return {
625
+ id: `${this.getConnectionInfo.activity_id}`,
626
+ objectType: objectType || 'Activity'
627
+ }
628
+ else return null
629
+ })()
630
+
631
+ // Add the parent key of the context activity
632
+ if (activityParent)
633
+ stmt.context.contextActivities['parent'] = [activityParent]
634
+
635
+ //===================== contextActivity grouping =====================//
636
+
637
+ //Defining the Grouping of the context activity
638
+ const activityGrouping = (() => {
639
+ if (
640
+ activityParent &&
641
+ crsParams.courseID &&
642
+ activityParent.id.includes(crsParams.id)
643
+ )
644
+ return {
645
+ id: this.getConnectionInfo.activity_id.replace(
646
+ `/${crsParams.id}`,
647
+ ''
648
+ )
649
+ }
650
+ else return null
651
+ })()
652
+ //Adding the grouping of the context activity
653
+ if (activityGrouping)
654
+ stmt.context.contextActivities['grouping'] = [activityGrouping]
655
+
656
+ //add result data to statement
657
+ if (result) stmt['result'] = result
658
+
659
+ // Add duration info to statement when activity is complete
660
+ if (['completed', 'suspended', 'terminated'].includes(verb)) {
661
+ if (!stmt.result) stmt.result = {} // check if exist
662
+
663
+ let d
664
+
665
+ duration
666
+ ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
667
+ duration.split(':')[2]
668
+ }S`)
669
+ : (d = `PT${
670
+ this.appTimer
671
+ .ISOTimeParser(this.activityDuration)
672
+ .split(':')[0]
673
+ }H${
674
+ this.appTimer
675
+ .ISOTimeParser(this.activityDuration)
676
+ .split(':')[1]
677
+ }M${
678
+ this.appTimer
679
+ .ISOTimeParser(this.activityDuration)
680
+ .split(':')[2]
681
+ }S`)
682
+
683
+ stmt.result['duration'] = d
684
+ //Set the completion status of the result
685
+ if (completion) stmt.result['completion'] = completion
686
+ }
687
+
688
+ if (
689
+ extensions &&
690
+ extensions.constructor === Array &&
691
+ extensions.length > 0
692
+ ) {
693
+ // ===================== Extension of the Object definition =====================//
694
+ //Validate each entry given in the extension Array
695
+ extensions.forEach((e) => {
696
+ //entry must be of type Object
697
+ if (e.constructor !== Object)
698
+ throw new Error(`'${e}' is not a valid value. Must be Object`)
699
+
700
+ //Entry Must have id and content keys
701
+ const validKey = ['id', 'content']
702
+ Object.keys(e).forEach((key) => {
703
+ if (!validKey.includes(key))
704
+ throw new Error(`Not valid key '${key}' for entry ${e}`)
705
+
706
+ //id must be a String
707
+ if (key === 'id' && e[key] && e[key].constructor !== String)
708
+ throw new Error(`'${key}' must be of type String`)
339
709
  })
710
+
711
+ stmt.object.definition['extensions'] = {
712
+ ...stmt.object.definition['extensions'],
713
+ [`${activityId}/${e.id}`]: e.content
714
+ }
340
715
  })
341
716
  }
342
717
 
718
+ //============================================================
719
+ stmtsQueue.push(stmt)
720
+ })
721
+ this.$xapi._sendStatements(stmtsQueue, cb, withFetch)
722
+ }
723
+ },
724
+
725
+ /**
726
+ * @param {Function} cb
727
+ * @param {Bool} option - set if must use Fetch API or not. default = false
728
+ */
729
+ endLesson(cb, option = true) {
730
+ cb = cb || null
731
+ let text
732
+
733
+ //Defining the text to display for stmt description and definition
734
+ switch (this.$i18n.locale) {
735
+ case 'fr':
736
+ if (this.getModuleInfo.courseID)
737
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
738
+ else text = `Le ${this.getModuleInfo.id}`
739
+ break
740
+ case 'en':
741
+ if (this.getModuleInfo.courseID)
742
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
743
+ else text = `The ${this.getModuleInfo.id}`
343
744
  break
745
+ }
746
+
747
+ // Retrieve only user data in lessons front its interaction that we want to send to the LRS.
748
+ // Note: User Settings are sent on a different URI and statement
749
+ const { isFistTime, userSettings, ...lessonsData } =
750
+ this.getUserInteraction
344
751
 
345
- case 'xapi':
752
+ const stmt = {
753
+ verb: 'suspended',
754
+ definition: text,
755
+ description: text,
756
+ extensions: [
346
757
  {
347
- if (
348
- this.getConnectionInfo &&
349
- this.getConnectionInfo.actor &&
350
- this.getConnectionInfo.remote
351
- ) {
352
- //Try to get the user progress from LRS
353
- const userProgress = this.$xapi._getProgress(
354
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
355
- this.getConnectionInfo.activity_id
356
- )
357
- if (userProgress) {
358
- const { routeHistory, ...userData } = userProgress
359
- // console.log('Progress from Server: ', {
360
- // userData,
361
- // routeHistory
362
- // })
363
- this.$store.dispatch('setUserMetaData', userData) // update store record with existing record
364
- this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing record
365
- }
366
- this.updateTracker('appBase', 'ready')
367
- } else {
368
- //Get existing records for user data in local store
369
- this.$idb.openDB().then(() => {
370
- this.$idb
371
- .getFromDB(this.getModuleInfo.idbID)
372
- .then((res) => {
373
- if (res && res.$record) {
374
- const {
375
- routeHistory,
376
- userSettings,
377
- ...userData
378
- } = res.$record
379
-
380
- this.$store.dispatch('setUserMetaData', userData) // update store record with existing record
381
- this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing route info
382
-
383
- if (userSettings) {
384
- this.$store.dispatch(
385
- 'setApplicationSettings',
386
- userSettings
387
- ) // update store record with existing user setting
388
- }
389
- }
390
- })
391
- .then(() => this.updateTracker('appBase', 'ready'))
392
- })
758
+ id: 'ending-point',
759
+ content: (() =>
760
+ this.$route.name == 'menu'
761
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
762
+ ._namedRoute
763
+ : this.$route.name)()
764
+ },
765
+ {
766
+ id: 'user-data',
767
+ content: {
768
+ routeHistory: (() => {
769
+ const history = this.getRouteHistory.toReversed() //get the route history from the last
770
+
771
+ history[0] = this.$route.meta // change the last recored route to the current route
772
+
773
+ return history.toReversed()
774
+ })(),
775
+ ...lessonsData
776
+ }
777
+ }
778
+ ],
779
+ duration: this.appTimer.ISOTimeParser(this.lessonDuration)
780
+ }
781
+
782
+ const endStmt = { ...stmt }
783
+ endStmt.verb = 'terminated'
784
+ //================================STATEMENT FOR THE PLAYBAR ===============================================
785
+ //Creating custom statement
786
+ const stmtPlaybar = {
787
+ verb: 'played',
788
+ definition: text,
789
+ description: text,
790
+ extensions: [
791
+ {
792
+ id: 'playbar-values',
793
+ content: {
794
+ ...this.getMediaPlaybarValues()
795
+ }
796
+ }
797
+ ]
798
+ }
799
+ //================================STATEMENT FOR THE SETTINGS ===============================================
800
+
801
+ //Creating custom statement
802
+ const stmtUserSettings = {
803
+ id: (() => {
804
+ if (!this.getModuleInfo.courseID) return null
805
+ return this.getModuleInfo.courseID
806
+ })(),
807
+ verb: 'preferred',
808
+ definition: text,
809
+ description: text,
810
+ extensions: [
811
+ {
812
+ id: 'application-settings',
813
+ content: {
814
+ ...this.getApplicationSettings
393
815
  }
816
+ // content: {
817
+ // bookmark: false
818
+ // } //for testing
819
+ }
820
+ ]
821
+ }
822
+ //================================END STATEMENT ===============================================
823
+
824
+ this.sendXapiStatements(
825
+ [stmtPlaybar, stmtUserSettings, stmt, endStmt],
826
+ null,
827
+ option
828
+ ) //send xapi statement
829
+
830
+ if (this.appTimer.getTimerState() === 'started')
831
+ setTimeout(() => this.stopAppTimer(), 0) //clear the timer
832
+
833
+ if (cb) cb()
834
+ },
835
+
836
+ /** @description Method to reset the progression status of user
837
+ * the function reset first the user data in the app store then
838
+ * reset the idbStore if APP is running locally or prepare a statement with new userData Object to be sent over the LRS
839
+ * for the connected userAgent, activity if APP is Running online
840
+ * @event 'send-xapi-statement' emit to AppBaseModule.vue that will be executed by sendXapiStatement(s)
841
+ */
842
+ async resetUserData() {
843
+ this.$bus.$emit('set-comp-status', 'appBase', 'loading')
844
+ this.setUserMetaData({}) // resetting store record with existing for user data
845
+ this.setApplicationSettings({}) // resetting store record for settings
846
+ this.setRouteHistory([]) // resetting store record for all last visited pages
847
+
848
+ // this.$bus.$emit('stop-timer')
849
+ this.stopAppTimer()
850
+ if (this.getModuleInfo.packageType !== 'xapi') return
851
+
852
+ if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
853
+ return this.$idb.deleteDataInDB(this.getModuleInfo.idbID) //Must call the idb delete methode to reset indexDB store
854
+
855
+ //Send a completion statement for the current activity
856
+ let aTitle = `${this.$t('text.activity')} ${this.getConnectionInfo.activity_id}`
857
+
858
+ //Custom text for description and definition of the stament to be sent
859
+ let text
860
+ this.$i18n.locale == 'fr'
861
+ ? (text = `L'${aTitle} de ${this.getModuleInfo.id}`)
862
+ : (text = `The ${aTitle} of ${this.getModuleInfo.id}`)
863
+
864
+ /*Dispatch a send statement event to method AppBaseModule sendXapiStatement(s)
865
+ Content values are:
866
+ User data URI: "host_address/course_ID/Lesson_ID/user-data" ex: "http://localhost:8080/330N01FD6001/m1l1/ // user-data" and
867
+ User Bookmark URI: "host_address/course_ID/Lesson_ID/ending-point" ex: ""http://localhost:8080/330N01FD6001/m1l1/ending-point": "introduction"
868
+ */
869
+ const baseStmt = {
870
+ definition: text,
871
+ description: text
872
+ }
873
+ const exitStmt = {
874
+ ...baseStmt,
875
+ verb: 'exited'
876
+ }
877
+
878
+ const endStmt = {
879
+ ...baseStmt,
880
+ verb: 'terminated',
881
+ extensions: [
882
+ {
883
+ id: 'ending-point',
884
+ content: (() =>
885
+ this.$route.name == 'menu'
886
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
887
+ ._namedRoute
888
+ : this.$route.name)()
889
+ },
890
+ {
891
+ id: 'user-data',
892
+ content: new Object()
893
+ }
894
+ ],
895
+ duration: null,
896
+ completion: false // Resetting completion state value to
897
+ }
898
+ //================================STATEMENT FOR THE SETTINGS ===============================================
899
+
900
+ //Creating custom statement
901
+ const stmtUserSettings = {
902
+ ...baseStmt,
903
+ id: (() => {
904
+ if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
905
+ else return null
906
+ })(),
907
+
908
+ verb: 'preferred',
909
+ extensions: [
910
+ {
911
+ id: 'application-settings',
912
+ content: new Object()
394
913
  }
914
+ ]
915
+ }
916
+ //================================END STATEMENT ===============================================
917
+
918
+ this.sendXapiStatements([stmtUserSettings, exitStmt, endStmt])
919
+
920
+ this.$bus.$emit('set-comp-status', 'appBase', 'ready')
921
+ const actorMbox = this.getConnectionInfo.actor.mbox.replace('mailto:', '')
922
+ const activityId = this.getConnectionInfo.activity_id
923
+ // Creating a asynchronous fecth method that would resolve after a 2 second
924
+ const fetchData = async () => {
925
+ //funtion that will make a multiple request on the server
926
+ const requests = async () => {
927
+ const _url = new URL(activityId)
928
+ const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
929
+
930
+ const lessonProgressParam = { email: actorMbox, activityId }
931
+ const lessonStateParam = {
932
+ email: actorMbox,
933
+ activityId,
934
+ verb: 'suspended'
935
+ }
936
+ const lessonPosionParam = {
937
+ email: actorMbox,
938
+ activityId
939
+ }
940
+ const playbarParam = {
941
+ email: actorMbox,
942
+ activityId,
943
+ verb: 'played'
944
+ }
945
+ const preferencesParam = {
946
+ email: actorMbox,
947
+ activityId: parentID,
948
+ verb: 'preferred'
949
+ }
950
+
951
+ const fetchParams = [
952
+ lessonProgressParam,
953
+ lessonStateParam,
954
+ lessonPosionParam,
955
+ playbarParam,
956
+ preferencesParam
957
+ ]
958
+
959
+ // Using Promise.all to make parallel requests
960
+ const {
961
+ userData,
962
+ savedPoint,
963
+ preferredSettings,
964
+ playbarValues,
965
+ lessonStatus
966
+ } = await this.$xapi._getBulkData(fetchParams)
967
+
968
+ return {
969
+ userData,
970
+ savedPoint,
971
+ preferredSettings,
972
+ playbarValues,
973
+ lessonStatus
974
+ }
975
+ }
976
+ return new Promise((resolve) => {
977
+ setTimeout(() => resolve(requests()), 2000)
978
+ })
979
+ }
980
+
981
+ // Make request over user current data after sending statement to assure that the resetting worked
982
+
983
+ const usrData = await fetchData()
984
+ this.$bus.$emit('reset-complete')
985
+ console.warn('😄 USER STATUS: ', {
986
+ userID: actorMbox,
987
+ activityId,
988
+ Record: usrData
989
+ })
990
+ },
991
+
992
+ /**
993
+ * @description- Scroll directly to the target* containt
994
+ * target content can be any Node defined by the ID
995
+ * @param {String} target = id of HTMLElement. if target don't existe will scroll to wrapper-content
996
+ * @param {Obj} opt = scroll behavior to apply default { top: 0, left: 0, behavior: 'auto' }
997
+ */
998
+
999
+ moveTo(target, opt = { top: 0, left: 0, behavior: 'auto' }) {
1000
+ if (target.constructor !== String)
1001
+ throw new Error(
1002
+ '⚠️ Not supported value for @target. Must be of type String'
1003
+ )
1004
+
1005
+ let skipTo = document.querySelector(`#wrapper-content`) //default definition of main element
1006
+
1007
+ if (target) {
1008
+ let targetEl = document.querySelector(`#${target}`) // search for node element specified as main
1009
+
1010
+ if (targetEl) skipTo = targetEl
1011
+ }
1012
+
1013
+ if (skipTo) opt.top = skipTo.offsetTop + opt.top // Set scroll top from target top
1014
+
1015
+ if (skipTo) skipTo.setAttribute('tabIndex', -1) //Allowing accessibility control with keyboard
1016
+ window.scrollTo(opt)
1017
+ if (skipTo) this.resetFocus(skipTo) //focus on the target
1018
+ },
1019
+
1020
+ /**
1021
+ * @description Reset the focus the element
1022
+ * @param {HTMLElement} e - element that will get focus
1023
+ */
1024
+ resetFocus(e) {
1025
+ if (e) e.focus()
1026
+ },
1027
+ /**
1028
+ * @description Method to validate that application settings are correctly
1029
+ * Opens error component when configurations are not correct
1030
+ *
1031
+ */
1032
+ checkForErrors() {
1033
+ let errMessage
395
1034
 
1035
+ if (validateAppContent(this.appConfig, true).length) {
1036
+ return (this.error = validateAppContent(this.appConfig, true))
1037
+ }
1038
+
1039
+ const { list: activitiesList } = this.getAllActivities()
1040
+ let { no_menu, is_single_activity } = this.appConfig
1041
+
1042
+ let noMenu = no_menu == undefined ? false : no_menu
1043
+
1044
+ let isSingleActivity =
1045
+ is_single_activity == undefined ? false : is_single_activity
1046
+
1047
+ let err
1048
+ if (this.getErrorMenu) err = true
1049
+ else err = false
1050
+
1051
+ let consoleMsg = ''
1052
+ //error if There is more than one activity when is_single_activity
1053
+ switch (true) {
1054
+ case isSingleActivity && activitiesList.size > 1:
1055
+ 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`
1056
+
1057
+ consoleMsg = `Cannot have more than 1 activity with this settings configuration. Either disable 💲is_single_activity or DELETE ALL others activities`
1058
+ this.error.push(errMessage)
1059
+ break
1060
+
1061
+ case isSingleActivity && !noMenu:
1062
+ 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>`
1063
+ consoleMsg = `Cannot have MENU with current settings configuration. Either set 💲no_menu:false or 💲is_single_activity:false`
1064
+ this.error.push(errMessage)
1065
+ break
1066
+
1067
+ case err:
1068
+ 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 `
1069
+ consoleMsg = this.getErrorMenu
1070
+ this.error.push(errMessage)
396
1071
  break
397
1072
  }
1073
+
1074
+ if (errMessage)
1075
+ return console.warn(
1076
+ `%c WARNING!>>> ${consoleMsg}`,
1077
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
1078
+ )
398
1079
  },
399
- updateTracker(name, status) {
400
- this.$store.dispatch('updateCompStatusTracker', { name, status })
1080
+ /**
1081
+ * @description Handle the keydown event to detect if F5 key was pressed
1082
+ * The function set the f5KeyPressed to true if F5 key was pressed
1083
+ * @param {Event} e - keydown event
1084
+ */
1085
+ handleF5KeyPressed(e) {
1086
+ this.f5KeyPressed = e.code == 'F5'
1087
+ },
1088
+
1089
+ /**
1090
+ * @description Handle the beforeunload event to save data when the user close the app or reload the app
1091
+ * The function check if the F5 key was pressed to avoid sending data when the user reload the app
1092
+ */
1093
+ handleBeforeUnload() {
1094
+ //prevent app to send/Save data when F5 is pressed to reload app
1095
+ if (this.f5KeyPressed) return
1096
+
1097
+ this.executeCloseEventTriggered()
401
1098
  }
402
1099
  }
403
1100
  }
@@ -405,7 +1102,17 @@ export default {
405
1102
  <style lang="scss">
406
1103
  #App-base {
407
1104
  width: 100%;
408
- height: 100vh;
1105
+ height: 100%;
1106
+ min-height: 100vh;
1107
+
1108
+ #overlay_loading {
1109
+ position: fixed !important;
1110
+ width: 100%;
1111
+ height: 100%;
1112
+ top: 0;
1113
+ left: 0;
1114
+ z-index: 9999;
1115
+ }
409
1116
 
410
1117
  #build-info {
411
1118
  opacity: 0.9;
@@ -426,189 +1133,37 @@ export default {
426
1133
  }
427
1134
  }
428
1135
 
429
- .module {
1136
+ .box {
430
1137
  width: 100%;
431
1138
  height: 100%;
432
1139
  position: relative;
433
- display: flex;
434
- flex-direction: row;
435
- align-items: stretch;
436
-
437
- .navbar {
438
- position: absolute;
439
- top: 0;
440
- left: 0;
441
- z-index: 10;
442
- display: flex;
443
- flex-direction: column;
444
- flex-wrap: wrap;
445
- align-content: start;
446
- width: 67px;
447
- height: 100%;
448
- }
449
-
450
- .module-wrapper {
451
- width: 100%;
452
- position: relative;
453
-
454
- #wrapper-content {
455
- width: 100%;
456
- position: relative;
457
- padding-right: 0 !important;
458
- padding-left: 0 !important;
459
-
460
- &.active {
461
- overflow: hidden;
462
- }
463
-
464
- .box {
465
- width: 100%;
466
- height: 95%;
467
-
468
- .app-page {
469
- width: 100%;
470
- margin-top: 60px;
471
-
472
- .row {
473
- width: 100%;
474
- margin-right: 0;
475
- margin-left: 0;
476
- }
477
- }
478
- }
479
- }
480
-
481
- .app-comp-table-of-content {
482
- display: -webkit-flex;
483
- display: flex;
484
- flex-direction: column;
485
- flex-wrap: wrap;
486
- position: absolute;
487
- top: 0px;
488
- min-height: 100%;
489
- z-index: 1;
490
- }
491
- }
492
-
493
- .app-nav {
494
- .md-controller {
495
- display: -webkit-flex;
496
- display: flex;
497
- flex-direction: row;
498
- align-items: baseline;
499
- //width: $widthPlayer;
500
- width: 50%;
501
-
502
- .ctrl-play {
503
- display: -webkit-flex;
504
- display: flex;
505
- flex-direction: row;
506
- align-items: baseline;
507
- //width: $widthPlayer;
508
- width: 50%;
509
-
510
- #progress-bar {
511
- display: block;
512
- //width: $widthProgressBar;
513
- width: 150px;
514
- //height: $heigthProgressBar;
515
- height: 10px;
516
- position: relative;
517
- overflow: hidden;
518
-
519
- #progress {
520
- display: block;
521
- height: 100%;
522
- background-color: red;
523
- }
524
-
525
- #progress-shaddow {
526
- display: block;
527
- height: 100%;
528
- position: absolute;
529
- top: 0px;
530
- left: 0px;
531
- z-index: -1;
532
- }
533
- }
534
- }
535
-
536
- .ctrl-subtitle {
537
- //width: $widthCtrSubtitle;
538
- width: 10%;
539
- }
540
-
541
- .ctrl-sound {
542
- display: -webkit-flex;
543
- display: flex;
544
- flex-direction: row;
545
- align-items: baseline;
546
- justify-content: space-between;
547
- //width: $widthCtrlSound;
548
- width: 40%;
549
- }
550
- }
551
- }
552
1140
  }
553
1141
  }
554
-
555
- .macWarn-overlay {
556
- position: absolute;
557
- top: 0px;
558
- left: 0px;
559
- z-index: 99999;
560
- width: 100%;
561
- height: 100vh;
562
- display: none;
563
- background-color: rgba(0, 0, 0, 0.92);
564
- opacity: 0;
565
- &.warningPortait {
566
- display: block;
567
- opacity: 1;
568
- }
569
-
570
- .macWarn-box {
571
- width: 50%;
572
- position: absolute;
573
-
574
- top: calc(45% - 15% / 2);
575
- left: 25%;
576
-
577
- h1 {
578
- text-align: center;
579
- color: #fff !important;
580
- }
581
-
582
- p {
583
- text-align: center;
584
- font-size: 1.3rem;
585
- color: #fff !important;
586
- }
587
-
588
- #btn-warning-ok {
589
- display: block;
590
- margin: 10px auto;
591
- }
1142
+ .grp-spinners {
1143
+ transform: scale(1.5);
1144
+ #icon_loading {
1145
+ animation: spin 1.3s cubic-bezier(0.57, 0.87, 0.77, 0.87) infinite;
592
1146
  }
593
1147
  }
594
- .fade-enter-active,
595
- .fade-leave-active {
596
- transition: opacity 0.3s ease;
597
- }
598
- .fade-enter, .fade-leave-to
599
- /* .component-fade-leave-active below version 2.1.8 */ {
600
- opacity: 0;
601
- }
602
-
603
- .app-icons-svg {
604
- width: 30px;
605
- height: 30px;
606
- fill: #fff;
607
- stroke: #fff;
608
- cursor: pointer;
1148
+ @keyframes spin {
1149
+ 100% {
1150
+ transform: rotate(360deg);
1151
+ }
609
1152
  }
610
1153
 
611
- .svg-hidden {
612
- display: none;
1154
+ .timer {
1155
+ position: sticky; /* Sticks inside the parent container */
1156
+ bottom: 90%; /* Sticks to the top edge of the parent */
1157
+ left: 100%; /* Sticks to the left edge of the parent */
1158
+ display: flex;
1159
+ align-items: center;
1160
+ justify-content: flex-start;
1161
+ font-size: 1em;
1162
+ width: fit-content;
1163
+ color: #333;
1164
+ opacity: 0.9;
1165
+ background-color: #eaabb6b3; /* Optional: ensures readability when content scrolls */
1166
+ padding: 5px 12px; /* Some breathing space */
1167
+ z-index: 1000; /* Keeps it above other elements */
613
1168
  }
614
1169
  </style>