fcad-core-dragon 2.1.0-beta.2 → 2.1.0-beta.3

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 (67) hide show
  1. package/.editorconfig +33 -33
  2. package/.eslintignore +29 -29
  3. package/.eslintrc.cjs +81 -81
  4. package/CHANGELOG +20 -0
  5. package/bk.scss +117 -117
  6. package/package.json +30 -31
  7. package/src/assets/data/onboardingMessages.json +47 -47
  8. package/src/components/AppBase.vue +167 -39
  9. package/src/components/AppBaseButton.test.js +0 -1
  10. package/src/components/AppBaseErrorDisplay.vue +438 -438
  11. package/src/components/AppBaseFlipCard.vue +84 -84
  12. package/src/components/AppBaseModule.vue +103 -116
  13. package/src/components/AppBasePage.vue +13 -13
  14. package/src/components/AppBasePopover.vue +41 -41
  15. package/src/components/AppCompMenu.vue +2 -1
  16. package/src/components/AppCompPlayBarNext.vue +157 -16
  17. package/src/components/AppCompPopUpNext.vue +3 -3
  18. package/src/components/AppCompQuizRecall.vue +2 -3
  19. package/src/components/AppCompSettingsMenu.vue +172 -172
  20. package/src/components/AppCompTableOfContent.vue +1 -1
  21. package/src/components/AppCompViewDisplay.vue +6 -6
  22. package/src/components/tests__/useTimer.spec.js +91 -0
  23. package/src/composables/useIdleDetector.js +56 -0
  24. package/src/composables/useQuiz.js +1 -1
  25. package/src/composables/useTimer.js +175 -0
  26. package/src/externalComps/ModuleView.vue +22 -22
  27. package/src/externalComps/SummaryView.vue +91 -91
  28. package/src/main.js +2 -0
  29. package/src/module/stores/appStore.js +10 -34
  30. package/src/module/xapi/ADL.js +1 -0
  31. package/src/module/xapi/Crypto/Hasher.js +241 -241
  32. package/src/module/xapi/Crypto/WordArray.js +278 -278
  33. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
  34. package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
  35. package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
  36. package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
  37. package/src/module/xapi/Crypto/encoders/Base.js +105 -105
  38. package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
  39. package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
  40. package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
  41. package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
  42. package/src/module/xapi/Crypto/index.js +53 -53
  43. package/src/module/xapi/Statement/activity.js +47 -47
  44. package/src/module/xapi/Statement/agent.js +55 -55
  45. package/src/module/xapi/Statement/group.js +26 -26
  46. package/src/module/xapi/Statement/index.js +259 -259
  47. package/src/module/xapi/Statement/statement.js +253 -253
  48. package/src/module/xapi/Statement/statementRef.js +23 -23
  49. package/src/module/xapi/Statement/substatement.js +22 -22
  50. package/src/module/xapi/Statement/verb.js +36 -36
  51. package/src/module/xapi/activitytypes.js +17 -17
  52. package/src/module/xapi/utils.js +167 -167
  53. package/src/module/xapi/verbs.js +294 -294
  54. package/src/module/xapi/xapiStatement.js +444 -444
  55. package/src/plugins/analytics.js +34 -0
  56. package/src/plugins/bus.js +8 -8
  57. package/src/plugins/gsap.js +14 -14
  58. package/src/plugins/i18n.js +26 -44
  59. package/src/plugins/save.js +37 -37
  60. package/src/plugins/scorm.js +287 -287
  61. package/src/plugins/xapi.js +11 -11
  62. package/src/public/index.html +33 -33
  63. package/src/router/index.js +6 -3
  64. package/src/components/AppCompPlayBarProgress.vue +0 -82
  65. package/src/mixins/$mediaMixins.js +0 -809
  66. package/src/mixins/timerMixin.js +0 -195
  67. package/src/module/xapi/wrapper copy.js +0 -1963
@@ -1,47 +1,47 @@
1
- {
2
- "message_1": {
3
- "type": "popup-avert",
4
- "value": {
5
- "title": "Bienvenue dans la visite guidée du FCAD !",
6
- "hypertext_1": "<p>Le bouton Confirmer vous fera passer à la prochaine étape de la visite.</p><p>Vous pouvez y mettre fin en tout temps avec le bouton Annuler.</p>"
7
- }
8
- },
9
- "message_2": {
10
- "type": "tooltip",
11
- "value": {
12
- "target": "activity_progress",
13
- "title": "Navigation secondaire",
14
- "content": "<p>Les flèches permettent de passer à la page précédente ou suivante de l'activité.</p><p>Les rectangles représentent une section de l'activité.</p>"
15
- }
16
- },
17
- "message_3": {
18
- "type": "tooltip",
19
- "value": {
20
- "target": "btn_back_summary",
21
- "title": "Navigation secondaire",
22
- "content": "<p>Ce bouton permet de retourner au sommaire de l'activité en tout temps.</p>"
23
- }
24
- },
25
- "message_4": {
26
- "type": "tooltip",
27
- "value": {
28
- "target": "primary_activity",
29
- "title": "Navigation principale",
30
- "content": "<p>La navigation principale permet de valider ses réponses de quiz et de passer à la page suivante.</p>"
31
- }
32
- },
33
- "message_5": {
34
- "type": "popup-avert",
35
- "value": {
36
- "title": "Visite guidée",
37
- "hypertext_1": "<p>Message pour la barre de lecture.</p>"
38
- }
39
- },
40
- "message_6": {
41
- "type": "popup-avert",
42
- "value": {
43
- "title": "Visite guidée",
44
- "hypertext_1": "<p>Message pour les Paramètres.</p>"
45
- }
46
- }
47
- }
1
+ {
2
+ "message_1": {
3
+ "type": "popup-avert",
4
+ "value": {
5
+ "title": "Bienvenue dans la visite guidée du FCAD !",
6
+ "hypertext_1": "<p>Le bouton Confirmer vous fera passer à la prochaine étape de la visite.</p><p>Vous pouvez y mettre fin en tout temps avec le bouton Annuler.</p>"
7
+ }
8
+ },
9
+ "message_2": {
10
+ "type": "tooltip",
11
+ "value": {
12
+ "target": "activity_progress",
13
+ "title": "Navigation secondaire",
14
+ "content": "<p>Les flèches permettent de passer à la page précédente ou suivante de l'activité.</p><p>Les rectangles représentent une section de l'activité.</p>"
15
+ }
16
+ },
17
+ "message_3": {
18
+ "type": "tooltip",
19
+ "value": {
20
+ "target": "btn_back_summary",
21
+ "title": "Navigation secondaire",
22
+ "content": "<p>Ce bouton permet de retourner au sommaire de l'activité en tout temps.</p>"
23
+ }
24
+ },
25
+ "message_4": {
26
+ "type": "tooltip",
27
+ "value": {
28
+ "target": "primary_activity",
29
+ "title": "Navigation principale",
30
+ "content": "<p>La navigation principale permet de valider ses réponses de quiz et de passer à la page suivante.</p>"
31
+ }
32
+ },
33
+ "message_5": {
34
+ "type": "popup-avert",
35
+ "value": {
36
+ "title": "Visite guidée",
37
+ "hypertext_1": "<p>Message pour la barre de lecture.</p>"
38
+ }
39
+ },
40
+ "message_6": {
41
+ "type": "popup-avert",
42
+ "value": {
43
+ "title": "Visite guidée",
44
+ "hypertext_1": "<p>Message pour les Paramètres.</p>"
45
+ }
46
+ }
47
+ }
@@ -36,7 +36,12 @@
36
36
  </transition>
37
37
 
38
38
  <router-view class="box" />
39
-
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>
44
+ </div>
40
45
  <app-icons-next :extra-icons="userExtraIcons" />
41
46
  </template>
42
47
 
@@ -61,18 +66,19 @@
61
66
  <script>
62
67
  import { mapState, mapActions } from 'pinia'
63
68
  import { useAppStore } from '../module/stores/appStore.js'
64
- import { timerMixin } from '../mixins/timerMixin'
69
+ import { Timer } from '../composables/useTimer.js'
70
+ import { IdleDetector } from '../composables/useIdleDetector.js'
65
71
  import { validateAppContent } from '../shared/validators'
66
- import { computed } from 'vue'
72
+ import { version as fcadVersion } from '../../package.json'
73
+ import { computed, h, reactive } from 'vue'
67
74
  import mobileDetect from 'mobile-detect'
75
+
68
76
  export default {
69
- mixins: [timerMixin],
70
77
  provide() {
71
78
  return {
72
- elapsedIdleTime: computed(() => this.elapsedIdleTime),
73
- timerCurrentState: computed(() => this.timerCurrentState),
74
- lessonDuration: computed(() => this.lessonDuration),
75
- activityDuration: computed(() => this.activityDuration)
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
76
82
  }
77
83
  },
78
84
  props: {
@@ -86,7 +92,9 @@ export default {
86
92
  },
87
93
  setup() {
88
94
  const store = useAppStore()
89
- return { store }
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 }
90
98
  },
91
99
 
92
100
  data() {
@@ -99,7 +107,10 @@ export default {
99
107
  buildInfoClicked: false,
100
108
  error: [],
101
109
  initialLoading: true,
102
- f5KeyPressed: false
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
103
114
  }
104
115
  },
105
116
  computed: {
@@ -117,8 +128,16 @@ export default {
117
128
  'getErrorMenu',
118
129
  'getRouteHistory',
119
130
  'getBookmarkEnabled',
131
+ 'getAppDebugMode',
120
132
  'getApplicationSettings'
121
133
  ]),
134
+ appDebugMode() {
135
+ return this.getAppDebugMode
136
+ },
137
+ activityDuration() {
138
+ return this.appTimer.getTime() // return the time duration in seconds
139
+ },
140
+
122
141
  getwidth() {
123
142
  return window.innerWidth
124
143
  },
@@ -134,12 +153,9 @@ export default {
134
153
  },
135
154
  appReady() {
136
155
  let readyState = this.getAppStatus === 'ready' ? true : false
137
- if (readyState) {
138
- this.setInitialLoadingDone()
139
- }
140
-
141
156
  return readyState
142
157
  },
158
+
143
159
  displayLang() {
144
160
  let lang = false
145
161
  const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
@@ -162,6 +178,20 @@ export default {
162
178
  }
163
179
  },
164
180
  watch: {
181
+ initialLoading: {
182
+ deep: true,
183
+ handler(newVal) {
184
+ if (newVal !== false) {
185
+ return
186
+ }
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
+ },
165
195
  'store.$state.userDataLoaded': {
166
196
  handler() {
167
197
  !this.store.$state.userDataLoaded
@@ -169,6 +199,13 @@ export default {
169
199
  : this.updateTracker('appBase', 'ready')
170
200
  },
171
201
  immediate: true
202
+ },
203
+ appReady: {
204
+ handler(newValue) {
205
+ if (newValue) {
206
+ this.setInitialLoadingDone()
207
+ }
208
+ }
172
209
  }
173
210
  },
174
211
 
@@ -203,24 +240,19 @@ export default {
203
240
  this.$bus.$on('fire-exit-event', this.endLesson)
204
241
  this.$bus.$on('reset-focus-on', this.resetFocus)
205
242
  this.$bus.$on('move-to-target', this.moveTo)
243
+ this.$analytics.init(this.getAppConfigs.analytics_id, this.$router)
206
244
  this.$bus.$on('start-timer', this.startAppTimer)
245
+ this.$bus.$on('stop-app-timer', this.stopAppTimer)
207
246
  this.$bus.$on('start-idle-detector', this.setIdleDetector)
208
247
  this.$bus.$on('stop-idle-detector', this.unsetIdleDetector)
209
248
  },
210
249
  beforeMount() {
211
- window.addEventListener('keydown', this.handleF5KeyPressed)
212
250
  window.addEventListener(
213
251
  'beforeunload',
214
252
  (e) => {
253
+ this.executeCloseEventTriggered()
215
254
  // e.preventDefault()
216
255
  // e.returnValue = true
217
-
218
- //prevent app to send/Save data when F5 is pressed to reload app
219
- if (this.f5KeyPressed) return
220
-
221
- this.executeCloseEventTriggered()
222
-
223
- //debugger
224
256
  },
225
257
  true
226
258
  )
@@ -230,6 +262,7 @@ export default {
230
262
  // set the language of the app
231
263
  this.setLocale(this.getAppConfigs.lang)
232
264
  window.addEventListener('keydown', this.handleF5KeyPressed)
265
+ window.addEventListener('beforeunload', this.handleBeforeUnload)
233
266
  },
234
267
  beforeUnmount() {
235
268
  this.$bus.$off('set-comp-status', this.updateTracker)
@@ -241,7 +274,9 @@ export default {
241
274
  this.$bus.$off('start-idle-detector', this.setIdleDetector)
242
275
  this.$bus.$off('stop-idle-detector', this.unsetIdleDetector)
243
276
  this.$bus.$off('start-timer', this.startAppTimer)
277
+ this.$bus.$off('stop-app-timer', this.stopAppTimer)
244
278
  window.removeEventListener('keydown', this.handleF5KeyPressed)
279
+ window.removeEventListener('beforeunload', this.handleBeforeUnload)
245
280
  if (this.getAppConfigs.remote && this.unsubscribeToSetConfig)
246
281
  this.unsubscribeToSetConfig() //stop watching changing in store to save to local storage
247
282
  },
@@ -258,30 +293,82 @@ export default {
258
293
  'setMobileState',
259
294
  'setCurrentBrowser'
260
295
  ]),
296
+ /**
297
+ * @description start the app timer instance
298
+ * Check if the timer is already started, if yes it add the activity duration
299
+ * to the lesson duration and restart the timer
300
+ */
301
+
261
302
  startAppTimer() {
262
- if (this.timerCurrentState == 'started') this.stopTimer('activity')
263
- this.startTimer('activity')
303
+ if (this.$route.name === 'module') return // don't start timer when in menu
304
+ if (this.appTimer.getTimerState() == 'started') {
305
+ this.lessonDuration += this.activityDuration //
306
+ this.appTimer.stop() // Start the activity timer
307
+ }
308
+
309
+ //Delay actually one second before starting timers
310
+ this.starterTimeout = setTimeout(() => {
311
+ this.appTimer.start() // Start the activity timer
312
+ this.setIdleDetector() //start the idle time detector
313
+ }, 500)
264
314
  },
265
315
 
316
+ /**
317
+ * @description stop the app timer instance
318
+ * The function add the activity duration to the lesson duration and stop the timer
319
+ */
320
+ stopAppTimer() {
321
+ this.lessonDuration += this.activityDuration
322
+ this.appTimer.stop() // stop timer
323
+ clearTimeout(this.starterTimeout)
324
+ },
325
+
326
+ /**
327
+ * @description set the idle detector to start detecting idle time
328
+ * Attach DOM event listeners to reset the idle timer
329
+ * The idle detector will start after 1 second after the app is loaded
330
+ * The idle detector will pause the app timer when idle time is reached
331
+ */
332
+
266
333
  setIdleDetector() {
267
334
  setTimeout(() => {
268
- this.startIdleTimer() //start the iddle dectection
335
+ this.idleDetector.startIdleTimer(
336
+ () => this.appTimer.pause(),
337
+ this.idleTimeout
338
+ )
269
339
  document.addEventListener('mousedown', this.resetIdleTimer, false) //set eventlisteners
270
340
  document.addEventListener('mousemove', this.resetIdleTimer, false) //set eventlisteners
271
341
  document.addEventListener('keypress', this.resetIdleTimer, false) //set eventlisteners
272
342
  document.addEventListener('touchmove', this.resetIdleTimer, false) //set eventlisteners
273
- }, 1000)
343
+ }, 800)
274
344
  },
275
- /*
276
- * stop the idle timer and remove hidle-dector listeners
345
+ /**
346
+ * @description unset the idle detector
347
+ * stop the idle timer and remove DOM event to stop listening the idle timer
277
348
  */
278
349
  unsetIdleDetector() {
279
- this.stopIdleTimer()
350
+ this.idleDetector.stopIdleTimer()
280
351
  document.removeEventListener('mousedown', this.resetIdleTimer, false)
281
352
  document.removeEventListener('mousemove', this.resetIdleTimer, false)
282
353
  document.removeEventListener('keypress', this.resetIdleTimer, false)
283
354
  document.removeEventListener('touchmove', this.resetIdleTimer, false)
284
355
  },
356
+ /**
357
+ * @description reset the idle timer
358
+ * The function stop the idle timer and restart it
359
+ * start also the app timer if it was paused
360
+ *
361
+ */
362
+ resetIdleTimer() {
363
+ if (this.$route.name == 'menu' || this.$route.name == 'module') return // don't reset idle timer when in menu
364
+ this.idleDetector.stopIdleTimer()
365
+
366
+ if (this.appTimer.getTimerState() == 'stopped') this.appTimer.start()
367
+ this.idleDetector.startIdleTimer(
368
+ () => this.appTimer.pause(),
369
+ this.idleTimeout
370
+ )
371
+ },
285
372
 
286
373
  setInitialLoadingDone() {
287
374
  this.initialLoading = false
@@ -580,21 +667,31 @@ export default {
580
667
  ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
581
668
  duration.split(':')[2]
582
669
  }S`)
583
- : (d = `PT${this.activityDuration.split(':')[0]}H${
584
- this.activityDuration.split(':')[1]
585
- }M${this.activityDuration.split(':')[2]}S`)
670
+ : (d = `PT${
671
+ this.appTimer
672
+ .ISOTimeParser(this.activityDuration)
673
+ .split(':')[0]
674
+ }H${
675
+ this.appTimer
676
+ .ISOTimeParser(this.activityDuration)
677
+ .split(':')[1]
678
+ }M${
679
+ this.appTimer
680
+ .ISOTimeParser(this.activityDuration)
681
+ .split(':')[2]
682
+ }S`)
586
683
 
587
684
  stmt.result['duration'] = d
588
685
  //Set the completion status of the result
589
686
  if (completion) stmt.result['completion'] = completion
590
687
  }
591
688
 
592
- // ===================== Extension of the Object definition =====================//
593
689
  if (
594
690
  extensions &&
595
691
  extensions.constructor === Array &&
596
692
  extensions.length > 0
597
693
  ) {
694
+ // ===================== Extension of the Object definition =====================//
598
695
  //Validate each entry given in the extension Array
599
696
  extensions.forEach((e) => {
600
697
  //entry must be of type Object
@@ -622,7 +719,6 @@ export default {
622
719
  //============================================================
623
720
  stmtsQueue.push(stmt)
624
721
  })
625
-
626
722
  this.$xapi._sendStatements(stmtsQueue, cb, withFetch)
627
723
  }
628
724
  },
@@ -681,7 +777,7 @@ export default {
681
777
  }
682
778
  }
683
779
  ],
684
- duration: this.lessonDuration
780
+ duration: this.appTimer.ISOTimeParser(this.lessonDuration)
685
781
  }
686
782
 
687
783
  const endStmt = { ...stmt }
@@ -732,7 +828,8 @@ export default {
732
828
  option
733
829
  ) //send xapi statement
734
830
 
735
- if (this.timerState === 'started') setTimeout(() => this.stopTimer(), 0) //clear the timer
831
+ if (this.appTimer.getTimerState() === 'started')
832
+ setTimeout(() => this.stopAppTimer(), 0) //clear the timer
736
833
 
737
834
  if (cb) cb()
738
835
  },
@@ -750,7 +847,7 @@ export default {
750
847
  this.setRouteHistory([]) // resetting store record for all last visited pages
751
848
 
752
849
  // this.$bus.$emit('stop-timer')
753
- this.stopTimer()
850
+ this.stopAppTimer()
754
851
  if (this.getModuleInfo.packageType !== 'xapi') return
755
852
 
756
853
  if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
@@ -929,7 +1026,7 @@ export default {
929
1026
  if (e) e.focus()
930
1027
  },
931
1028
  /**
932
- * @description Mothods to validate that application settings are correctly
1029
+ * @description Method to validate that application settings are correctly
933
1030
  * Opens error component when configurations are not correct
934
1031
  *
935
1032
  */
@@ -981,9 +1078,24 @@ export default {
981
1078
  'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
982
1079
  )
983
1080
  },
984
-
1081
+ /**
1082
+ * @description Handle the keydown event to detect if F5 key was pressed
1083
+ * The function set the f5KeyPressed to true if F5 key was pressed
1084
+ * @param {Event} e - keydown event
1085
+ */
985
1086
  handleF5KeyPressed(e) {
986
1087
  this.f5KeyPressed = e.code == 'F5'
1088
+ },
1089
+
1090
+ /**
1091
+ * @description Handle the beforeunload event to save data when the user close the app or reload the app
1092
+ * The function check if the F5 key was pressed to avoid sending data when the user reload the app
1093
+ */
1094
+ handleBeforeUnload() {
1095
+ //prevent app to send/Save data when F5 is pressed to reload app
1096
+ if (this.f5KeyPressed) return
1097
+
1098
+ this.executeCloseEventTriggered()
987
1099
  }
988
1100
  }
989
1101
  }
@@ -1039,4 +1151,20 @@ export default {
1039
1151
  transform: rotate(360deg);
1040
1152
  }
1041
1153
  }
1154
+
1155
+ .timer {
1156
+ position: sticky; /* Sticks inside the parent container */
1157
+ bottom: 90%; /* Sticks to the top edge of the parent */
1158
+ left: 100%; /* Sticks to the left edge of the parent */
1159
+ display: flex;
1160
+ align-items: center;
1161
+ justify-content: flex-start;
1162
+ font-size: 1em;
1163
+ width: fit-content;
1164
+ color: #333;
1165
+ opacity: 0.9;
1166
+ background-color: #eaabb6b3; /* Optional: ensures readability when content scrolls */
1167
+ padding: 5px 12px; /* Some breathing space */
1168
+ z-index: 1000; /* Keeps it above other elements */
1169
+ }
1042
1170
  </style>
@@ -11,7 +11,6 @@ describe('AppBaseButton Tests', () => {
11
11
  })
12
12
  expect(wrapper.find('button').exists()).toBeTruthy()
13
13
  })
14
-
15
14
  it('should fire click event', async () => {
16
15
  const wrapper = mount(AppBaseButton, {
17
16
  props: defaultProps