fcad-core-dragon 2.0.0-beta.9 → 2.0.0

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.
package/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ 2.0.0(31 mars 2025)
2
+ Menu : ponctuation des titres selon la langue (fr/en)
3
+
4
+ Fenêtre latérale : Le minimum de "branches" est maintenant 1 (était 2)
5
+
6
+ Fenêtre latérale : Bogue quand on la referme avec espace
7
+
8
+ Nouvelle fonctionnalité : Idle detector
9
+
10
+ Fenêtre progression : Bogue avec le path "activité 99", maintenant "conclusion"
11
+
1
12
  2.0.0-beta.9(03 mars 2025)
2
13
  Correction de bogues (conflit toc/notes avec popup, ouverture simultanée de toc et notes)
3
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fcad-core-dragon",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./src/main.js",
@@ -62,10 +62,18 @@ import { mapState, mapActions } from 'pinia'
62
62
  import { useAppStore } from '../module/stores/appStore.js'
63
63
  import { timerMixin } from '../mixins/timerMixin'
64
64
  import { validateAppContent } from '../shared/validators'
65
-
65
+ import { computed } from 'vue'
66
66
  import mobileDetect from 'mobile-detect'
67
67
  export default {
68
68
  mixins: [timerMixin],
69
+ provide() {
70
+ return {
71
+ elapsedIdleTime: computed(() => this.elapsedIdleTime),
72
+ timerCurrentState: computed(() => this.timerCurrentState),
73
+ lessonDuration: computed(() => this.lessonDuration),
74
+ activityDuration: computed(() => this.activityDuration)
75
+ }
76
+ },
69
77
  props: {
70
78
  appConfig: {
71
79
  type: Object,
@@ -221,6 +229,20 @@ export default {
221
229
  }
222
230
  }
223
231
  }
232
+ //===========FOR TESTING TIMER TIMEOUT====//
233
+ // elapsedIdleTime: {
234
+ // handler() {
235
+ // console.log(
236
+ // '⌛:',
237
+ // this.elapsedIdleTime,
238
+ // this.timerCurrentState,
239
+ // this.activityDuration
240
+ // )
241
+
242
+ // //max idle time allowed 300s (5 min)
243
+ // },
244
+ // immediate: true
245
+ // }
224
246
  },
225
247
 
226
248
  created() {
@@ -254,6 +276,9 @@ export default {
254
276
  this.$bus.$on('fire-exit-event', this.endLesson)
255
277
  this.$bus.$on('reset-focus-on', this.resetFocus)
256
278
  this.$bus.$on('move-to-target', this.moveTo)
279
+ this.$bus.$on('start-timer', this.startAppTimer)
280
+ this.$bus.$on('start-idle-detector', this.setIdleDetector)
281
+ this.$bus.$on('stop-idle-detector', this.unsetIdleDetector)
257
282
  },
258
283
  beforeMount() {
259
284
  window.addEventListener(
@@ -278,7 +303,9 @@ export default {
278
303
  this.$bus.$off('reset-focus-on', this.resetFocus)
279
304
  this.$bus.$off('send-xapi-statement', this.sendXapiStatements)
280
305
  this.$bus.$off('move-to-target', this.moveTo)
281
-
306
+ this.$bus.$off('start-idle-detector', this.setIdleDetector)
307
+ this.$bus.$off('stop-idle-detector', this.unsetIdleDetector)
308
+ this.$bus.$off('start-timer', this.startAppTimer)
282
309
  if (this.getAppConfigs.remote) this.unsubscribeToSetConfig() //stop watching changing in store to save to local storage
283
310
  },
284
311
  methods: {
@@ -294,6 +321,31 @@ export default {
294
321
  'setMobileState',
295
322
  'setCurrentBrowser'
296
323
  ]),
324
+ startAppTimer() {
325
+ if (this.timerCurrentState == 'started') this.stopTimer('activity')
326
+ this.startTimer('activity')
327
+ },
328
+
329
+ setIdleDetector() {
330
+ setTimeout(() => {
331
+ this.startIdleTimer() //start the iddle dectection
332
+ document.addEventListener('mousedown', this.resetIdleTimer, false) //set eventlisteners
333
+ document.addEventListener('mousemove', this.resetIdleTimer, false) //set eventlisteners
334
+ document.addEventListener('keypress', this.resetIdleTimer, false) //set eventlisteners
335
+ document.addEventListener('touchmove', this.resetIdleTimer, false) //set eventlisteners
336
+ }, 1000)
337
+ },
338
+ /*
339
+ * stop the idle timer and remove hidle-dector listeners
340
+ */
341
+ unsetIdleDetector() {
342
+ this.stopIdleTimer()
343
+ document.removeEventListener('mousedown', this.resetIdleTimer, false)
344
+ document.removeEventListener('mousemove', this.resetIdleTimer, false)
345
+ document.removeEventListener('keypress', this.resetIdleTimer, false)
346
+ document.removeEventListener('touchmove', this.resetIdleTimer, false)
347
+ },
348
+
297
349
  setInitialLoadingDone() {
298
350
  this.initialLoading = false
299
351
  },
@@ -422,6 +474,80 @@ export default {
422
474
 
423
475
  const { routeHistory = [], ...userProgress } = userData
424
476
 
477
+ const completedState = lessonStatus || {}
478
+ const lessonPosition = savedPoint || ''
479
+ const applicationSettings = preferredSettings || {}
480
+ //====FOR TESTING====//
481
+ // console.log('💾 RECORDS ', {
482
+ // userProgress,
483
+ // routeHistory,
484
+ // lessonPosition,
485
+ // completedState,
486
+ // playbarValues,
487
+ // applicationSettings
488
+ // })
489
+ //Update the App Store data
490
+ this.updateDataFetchFromServer({
491
+ userProgress,
492
+ routeHistory,
493
+ lessonPosition,
494
+ completedState,
495
+ playbarValues,
496
+ applicationSettings
497
+ }).then(() => {
498
+ this.updateTracker('appBase_fetch', 'ready')
499
+ })
500
+ },
501
+
502
+ async fetchDataFromServerOptimized() {
503
+ this.updateTracker('appBase_fetch', 'loading')
504
+ if (this.getModuleInfo.packageType !== 'xapi') return
505
+ if (!this.getConnectionInfo || !this.getConnectionInfo.remote) return
506
+
507
+ const actorMbox = this.getConnectionInfo.actor.mbox.replace('mailto:', '')
508
+ const activityId = this.getConnectionInfo.activity_id
509
+ const _url = new URL(activityId)
510
+ const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
511
+
512
+ const lessonProgressParam = { email: actorMbox, activityId }
513
+ const lessonStateParam = {
514
+ email: actorMbox,
515
+ activityId,
516
+ verb: 'completed'
517
+ }
518
+ const lessonPosionParam = {
519
+ email: actorMbox,
520
+ activityId
521
+ }
522
+ const playbarParam = {
523
+ email: actorMbox,
524
+ activityId,
525
+ verb: 'played'
526
+ }
527
+ const preferencesParam = {
528
+ email: actorMbox,
529
+ activityId: parentID,
530
+ verb: 'preferred'
531
+ }
532
+
533
+ const fetchParams = [
534
+ lessonProgressParam,
535
+ lessonStateParam,
536
+ lessonPosionParam,
537
+ playbarParam,
538
+ preferencesParam
539
+ ]
540
+
541
+ const {
542
+ userData,
543
+ savedPoint,
544
+ preferredSettings,
545
+ playbarValues,
546
+ lessonStatus
547
+ } = await this.$xapi._getBulkData(fetchParams)
548
+
549
+ const { routeHistory = [], ...userProgress } = userData
550
+
425
551
  const completedState = lessonStatus || {}
426
552
  const lessonPosition = savedPoint || ''
427
553
  const applicationSettings = preferredSettings || {}
@@ -929,7 +1055,8 @@ export default {
929
1055
  this.setApplicationSettings({}) // resetting store record for settings
930
1056
  this.setRouteHistory([]) // resetting store record for all last visited pages
931
1057
 
932
- this.$bus.$emit('stop-timer')
1058
+ // this.$bus.$emit('stop-timer')
1059
+ this.stopTimer()
933
1060
  if (this.getModuleInfo.packageType !== 'xapi') return
934
1061
 
935
1062
  if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
@@ -104,7 +104,6 @@ import { fileAssets } from '../shared/generalfuncs.js'
104
104
  import { mapState, mapActions } from 'pinia'
105
105
  import { useAppStore } from '../module/stores/appStore'
106
106
  import BaseModule from './BaseModule.vue'
107
- import { timerMixin } from '../mixins/timerMixin'
108
107
  import AppCompContainer from './AppCompContainer.vue'
109
108
  import { defineAsyncComponent } from 'vue'
110
109
  //import
@@ -113,7 +112,7 @@ export default {
113
112
  BaseModule,
114
113
  AppCompContainer
115
114
  },
116
- mixins: [timerMixin],
115
+ inject: ['elapsedIdleTime', 'timerCurrentState', 'lessonDuration'],
117
116
  props: {
118
117
  moduleConfig: {
119
118
  type: Object,
@@ -135,8 +134,6 @@ export default {
135
134
  popupIsOpen: false,
136
135
  hidePlayBar: false, // Controle visibility of the play bar. set to true to hide play bar
137
136
  stmt: null, // holder for xapi statememt,
138
- moduleTimer: 0, //tracker for overall time spent the lesson
139
- activityTimer: 0, // tracker for the time spent on activity,
140
137
  routeChangeCounter: 0,
141
138
  toolTipTarget: '', //for the tool tip,
142
139
  onboardingMessages: {}, //for the onboarding @todo replace with default file
@@ -262,7 +259,6 @@ export default {
262
259
 
263
260
  return mainEl
264
261
  },
265
-
266
262
  dynamicSidebarContent() {
267
263
  if (
268
264
  !this.transcriptVisible &&
@@ -367,13 +363,12 @@ export default {
367
363
  */
368
364
 
369
365
  const trackedRouteType = ['introduction', 'conclusion', 'normal']
366
+
370
367
  if (trackedRouteType.includes(this.$route.meta.type)) {
371
368
  if (this.$route.name.includes('.')) return
372
369
 
373
370
  //Start the timer every time that there is a new activity
374
- if (this.timerState === 'started') this.stopTimer('activity') // reset the activity timer
375
- this.startTimer('activity')
376
-
371
+ this.$bus.$emit('start-timer')
377
372
  //Send statement when activity as changed
378
373
  if (this.activityHasChanged)
379
374
  this.sendStartStatement({ id: this.$route.meta.activity_ref })
@@ -408,13 +403,12 @@ export default {
408
403
  /**
409
404
  * @description Defined in timerMixin Watch The epalsed time for autosaving the user reached position
410
405
  */
411
- elapsedTime: {
406
+ elapsedIdleTime: {
412
407
  handler() {
413
- if (this.timerState !== 'started') return
408
+ if (this.timerCurrentState !== 'started') return
414
409
  //send a statement every x time (second)
415
410
  if (
416
- this.timerState === 'started' &&
417
- this.elapsedTime % 500 === 0 &&
411
+ this.elapsedIdleTime % 500 === 0 &&
418
412
  this.getModuleInfo.packageType === 'xapi' &&
419
413
  this.getConnectionInfo &&
420
414
  this.getConnectionInfo.actor &&
@@ -486,6 +480,7 @@ export default {
486
480
  this.$bus.$off('show-transcript', this.openTranscript)
487
481
  this.$bus.$off('send-completion-event', this.sendCompletionStatus)
488
482
  this.$bus.$off('send-starting-event', this.sendStartStatement)
483
+
489
484
  //nav mouseleave event
490
485
  let nav = document.getElementById('navTool')
491
486
  if (nav) {
@@ -549,7 +544,9 @@ export default {
549
544
  this.$bus.$on('send-completion-event', this.sendCompletionStatus)
550
545
  this.$bus.$on('send-starting-event', this.sendStartStatement)
551
546
 
552
- this.initLesson()
547
+ this.initLesson().then(() => {
548
+ this.$bus.$emit('start-idle-detector')
549
+ })
553
550
 
554
551
  if (this.navigationHistory.length != 0) {
555
552
  this.routeData = this.navigationHistory
@@ -664,11 +661,11 @@ export default {
664
661
  closeSidebar(ctx, wrapper = null) {
665
662
  //delay animation
666
663
  this.rightSidebarVisible = false //
667
- // setTimeout(() => {
668
- const rSidebar = document.querySelector('#right-sidebar') // the sidebar
669
- rSidebar.setAttribute('style', 'display:none')
670
- rSidebar.dispatchEvent(this.rightSidebarEvent) //this will allow to run the animation of sidebar closing 1rst
671
- // }, 100)
664
+ setTimeout(() => {
665
+ const rSidebar = document.querySelector('#right-sidebar') // the sidebar
666
+ rSidebar.setAttribute('style', 'display:none')
667
+ rSidebar.dispatchEvent(this.rightSidebarEvent) //this will allow to run the animation of sidebar closing 1rst
668
+ }, 100)
672
669
  this.resetFocus(this.lastInFocus)
673
670
  switch (ctx) {
674
671
  case 'ctxTranscript':
@@ -122,9 +122,9 @@ export default {
122
122
  let errMsg = false
123
123
 
124
124
  //card must have at least 2 elements
125
- if (dataArray.length < 2) {
125
+ if (dataArray.length < 1) {
126
126
  isValid = false
127
- errMsg = `\n card require at least 2 element`
127
+ errMsg = `\n card require at least 1 element`
128
128
  } else {
129
129
  for (let el of dataArray) {
130
130
  //Each element in card must be of type Object and can not be empty
@@ -148,10 +148,10 @@ export default {
148
148
  let isValid = true
149
149
  if (import.meta.env.DEV) {
150
150
  let errMsg = false
151
- //card must have at least 2 elements
152
- if (dataArray.length < 2) {
151
+ //card must have at least 1 elements
152
+ if (dataArray.length < 1) {
153
153
  isValid = false
154
- errMsg = `\n customButton require at least 2 element`
154
+ errMsg = `\n customButton require at least 1 element`
155
155
  } else {
156
156
  for (let el of dataArray) {
157
157
  //Each element in card must be of type Object and can not be empty
@@ -1,228 +1,238 @@
1
- <!--
2
- @ Description: This component is used to display and create the link's to all the activity creation in module.
3
- @ What it does: Goes trougth all the activity in the router and create a card that open the Table of content (appCompTableOfContent) that display the anchor.Display the title and subtitle enter in menu.json. Must be used with AppCompTableOfContent and AppCompMenu.
4
- -->
5
- <template>
6
- <v-row v-if="activities.length" class="box-msa">
7
- <v-col
8
- v-for="activity of activities"
9
- :key="activity.id"
10
- cols="6"
11
- class="menu-section-activity"
12
- :aria-describedby="activity.id + '-subMenu'"
13
- >
14
- <router-link
15
- :id="activity.id + '-subMenu'"
16
- :title="activity.title"
17
- :data-test="`item-menu-${activity.id}`"
18
- class="btn-menu"
19
- :to="{
20
- name: createRoutes(activity.id)
21
- }"
22
- @click="startActivity(activity)"
23
- >
24
- <div class="menu-card" tag="article">
25
- <v-row>
26
- <div v-if="activity.subtitle === undefined" class="title">
27
- <h4 v-html="activity.title"></h4>
28
- </div>
29
- <div v-else class="title">
30
- <h4 v-html="`${activity.title} : ${activity.subtitle}`"></h4>
31
- </div>
32
-
33
- <div class="cnt-time">
34
- <svg :aria-label="$t('label.timer')">
35
- <use href="#clock-icon" />
36
- </svg>
37
- <p class="time">
38
- {{ activity.time || '00:00' }}
39
- </p>
40
- </div>
41
- <div class="box-gauge">
42
- <app-comp-jauge
43
- :max-value="getActivitySize(activity.id)"
44
- :value="getPageComplete(activity.id)"
45
- />
46
- </div>
47
- </v-row>
48
- </div>
49
- </router-link>
50
- </v-col>
51
- </v-row>
52
- </template>
53
- <script>
54
- // ...
55
- import { mapState } from 'pinia'
56
- import { useAppStore } from '../module/stores/appStore'
57
- import AppCompJauge from './AppCompJauge.vue'
58
- export default {
59
- components: {
60
- AppCompJauge
61
- },
62
- computed: {
63
- ...mapState(useAppStore, [
64
- 'getAllActivities',
65
- 'getAllActivitiesState',
66
- 'getAllCompleted',
67
- 'getAppConfigs',
68
- 'getMenuSettings',
69
- 'getModuleInfo'
70
- ]),
71
- activities() {
72
- // get the data for list of the page for this module from the store and
73
- // get the route of the page from the router to build the menu with its routes
74
- let count = 0
75
- const collection = []
76
-
77
- this.getAllActivities().list.forEach((value, key) => {
78
- let theActivity,
79
- activityTitle,
80
- activitySubTitle,
81
- activityTime = null,
82
- activityPath = ''
83
-
84
- if (this.menuInfo[key]) {
85
- const { title, subTitle, time } = this.menuInfo[key] //get time subTitle time from menu info
86
- //set the title
87
- if (title && title != ' ') activityTitle = title
88
- //set the subtitle
89
- if (subTitle && subTitle != ' ') activitySubTitle = subTitle
90
- //set the time
91
- if (time && time != ' ') activityTime = time
92
- }
93
- // This is the Introduction
94
- if (key === 'A00') {
95
- activityTitle = activityTitle || this.$t('text.introduction')
96
- activityPath = 'introduction'
97
- } else if (key === 'A99') {
98
- activityTitle = activityTitle || this.$t('text.conclusion')
99
- activityPath = 'conclusion'
100
- } else {
101
- count++
102
- activityTitle =
103
- activityTitle || `${this.$t('text.activity')} ${count}`
104
- activityPath = `activite_${count}`
105
- }
106
-
107
- theActivity = {
108
- id: key,
109
- title: activityTitle,
110
- subtitle: activitySubTitle,
111
- time: activityTime,
112
- path: activityPath,
113
- data: value
114
- }
115
-
116
- collection.push(theActivity) // push the activity in the collection
117
- })
118
-
119
- return collection
120
- },
121
- menuInfo() {
122
- let menuInfo = this.getMenuSettings
123
- return menuInfo
124
- }
125
- },
126
- mounted() {},
127
- methods: {
128
- /**
129
- * @description Gives gives length of completed page for an activity
130
- * @param {String} idActivity
131
- * @returns {Number}
132
- */
133
- getPageComplete(idActivity) {
134
- /// give all the page that are complete to the gauge
135
- if (idActivity) {
136
- let completed = []
137
- if (this.getAllCompleted[idActivity]) {
138
- completed = this.getAllCompleted[idActivity] //get all completed page for the activity
139
- }
140
-
141
- return completed.length
142
- }
143
- },
144
- /**
145
- * @description give the size of the state of the activity
146
- * @param {String} activityID
147
- * @returns {Number} size
148
- */
149
- getActivitySize(activityID) {
150
- /*
151
- * Note:
152
- * Assaging by reference would change the property of the getters when the valued are changed in the new object in. * To prevent this behaviour we will make a deep copy of the getter
153
- * cf: https://reactgo.com/javascript-clone-object/
154
- */
155
-
156
- const allActivitiesState = JSON.parse(
157
- JSON.stringify(this.getAllActivitiesState)
158
- )
159
- let size = allActivitiesState[activityID]
160
- ? allActivitiesState[activityID].size
161
- : 0
162
-
163
- return size
164
- },
165
- createRoutes(data) {
166
- let activity
167
-
168
- if (data.charAt(1) == '0' || data.charAt(1) == 0) {
169
- activity = data.substr(2)
170
- } else {
171
- activity = data.substr(1)
172
- }
173
-
174
- if (activity == 0 && activity == '0') return `introduction`
175
- if (activity == '99') return `conclusion`
176
- else return `activite_${activity}`
177
- },
178
- startActivity(a) {
179
- //this.$bus.$emit('send-starting-event', a)
180
- }
181
- }
182
- }
183
- </script>
184
- <style lang="scss">
185
- .menu-card {
186
- width: 100%;
187
- padding: 24px;
188
-
189
- .v-row {
190
- margin: 0;
191
- flex-direction: column;
192
-
193
- .title {
194
- flex-grow: 4;
195
- width: 80%;
196
- margin-bottom: 30px;
197
-
198
- h3 {
199
- text-align: left;
200
- }
201
- }
202
-
203
- .cnt-time {
204
- display: flex;
205
- flex-direction: row;
206
- align-items: center;
207
- margin-bottom: 20px;
208
-
209
- svg {
210
- width: 18px;
211
- height: 18px;
212
- margin-right: 10px;
213
- }
214
- }
215
-
216
- .box-gauge {
217
- display: block;
218
- width: 100%;
219
-
220
- .box-g {
221
- display: flex;
222
- flex-direction: row;
223
- align-items: center;
224
- }
225
- }
226
- }
227
- }
228
- </style>
1
+ <!--
2
+ @ Description: This component is used to display and create the link's to all the activity creation in module.
3
+ @ What it does: Goes trougth all the activity in the router and create a card that open the Table of content (appCompTableOfContent) that display the anchor.Display the title and subtitle enter in menu.json. Must be used with AppCompTableOfContent and AppCompMenu.
4
+ -->
5
+ <template>
6
+ <v-row v-if="activities.length" class="box-msa">
7
+ <v-col
8
+ v-for="activity of activities"
9
+ :key="activity.id"
10
+ cols="6"
11
+ class="menu-section-activity"
12
+ :aria-describedby="activity.id + '-subMenu'"
13
+ >
14
+ <router-link
15
+ :id="activity.id + '-subMenu'"
16
+ :title="activity.title"
17
+ :data-test="`item-menu-${activity.id}`"
18
+ class="btn-menu"
19
+ :to="{
20
+ name: createRoutes(activity.id)
21
+ }"
22
+ @click="startActivity(activity)"
23
+ >
24
+ <div class="menu-card" tag="article">
25
+ <v-row>
26
+ <div v-if="activity.subtitle === undefined" class="title">
27
+ <h4 v-html="activity.title"></h4>
28
+ </div>
29
+ <div v-else class="title">
30
+ <h4
31
+ v-html="
32
+ `${activity.title}${cardTitleSeparator}${activity.subtitle}`
33
+ "
34
+ ></h4>
35
+ </div>
36
+
37
+ <div class="cnt-time">
38
+ <svg :aria-label="$t('label.timer')">
39
+ <use href="#clock-icon" />
40
+ </svg>
41
+ <p class="time">
42
+ {{ activity.time || '00:00' }}
43
+ </p>
44
+ </div>
45
+ <div class="box-gauge">
46
+ <app-comp-jauge
47
+ :max-value="getActivitySize(activity.id)"
48
+ :value="getPageComplete(activity.id)"
49
+ />
50
+ </div>
51
+ </v-row>
52
+ </div>
53
+ </router-link>
54
+ </v-col>
55
+ </v-row>
56
+ </template>
57
+ <script>
58
+ // ...
59
+ import { mapState } from 'pinia'
60
+ import { useAppStore } from '../module/stores/appStore'
61
+ import AppCompJauge from './AppCompJauge.vue'
62
+ export default {
63
+ components: {
64
+ AppCompJauge
65
+ },
66
+ computed: {
67
+ ...mapState(useAppStore, [
68
+ 'getAllActivities',
69
+ 'getAllActivitiesState',
70
+ 'getAllCompleted',
71
+ 'getAppConfigs',
72
+ 'getMenuSettings',
73
+ 'getModuleInfo'
74
+ ]),
75
+ /*
76
+ * Apply different punctuation for colons. No space before a colon in english, one space before a colon in french
77
+ */
78
+ cardTitleSeparator() {
79
+ return this.getAppConfigs.lang === 'en' ? ': ' : ' : '
80
+ },
81
+ activities() {
82
+ // get the data for list of the page for this module from the store and
83
+ // get the route of the page from the router to build the menu with its routes
84
+ let count = 0
85
+ const collection = []
86
+
87
+ this.getAllActivities().list.forEach((value, key) => {
88
+ let theActivity,
89
+ activityTitle,
90
+ activitySubTitle,
91
+ activityTime = null,
92
+ activityPath = ''
93
+
94
+ if (this.menuInfo[key]) {
95
+ const { title, subTitle, time } = this.menuInfo[key] //get time subTitle time from menu info
96
+ //set the title
97
+ if (title && title != ' ') activityTitle = title
98
+ //set the subtitle
99
+ if (subTitle && subTitle != ' ') activitySubTitle = subTitle
100
+ //set the time
101
+ if (time && time != ' ') activityTime = time
102
+ }
103
+ // This is the Introduction
104
+ if (key === 'A00') {
105
+ activityTitle = activityTitle || this.$t('text.introduction')
106
+ activityPath = 'introduction'
107
+ } else if (key === 'A99') {
108
+ activityTitle = activityTitle || this.$t('text.conclusion')
109
+ activityPath = 'conclusion'
110
+ } else {
111
+ count++
112
+ activityTitle =
113
+ activityTitle || `${this.$t('text.activity')} ${count}`
114
+ activityPath = `activite_${count}`
115
+ }
116
+
117
+ theActivity = {
118
+ id: key,
119
+ title: activityTitle,
120
+ subtitle: activitySubTitle,
121
+ time: activityTime,
122
+ path: activityPath,
123
+ data: value
124
+ }
125
+
126
+ collection.push(theActivity) // push the activity in the collection
127
+ })
128
+
129
+ return collection
130
+ },
131
+ menuInfo() {
132
+ let menuInfo = this.getMenuSettings
133
+ return menuInfo
134
+ }
135
+ },
136
+ mounted() {},
137
+ methods: {
138
+ /**
139
+ * @description Gives gives length of completed page for an activity
140
+ * @param {String} idActivity
141
+ * @returns {Number}
142
+ */
143
+ getPageComplete(idActivity) {
144
+ /// give all the page that are complete to the gauge
145
+ if (idActivity) {
146
+ let completed = []
147
+ if (this.getAllCompleted[idActivity]) {
148
+ completed = this.getAllCompleted[idActivity] //get all completed page for the activity
149
+ }
150
+
151
+ return completed.length
152
+ }
153
+ },
154
+ /**
155
+ * @description give the size of the state of the activity
156
+ * @param {String} activityID
157
+ * @returns {Number} size
158
+ */
159
+ getActivitySize(activityID) {
160
+ /*
161
+ * Note:
162
+ * Assaging by reference would change the property of the getters when the valued are changed in the new object in. * To prevent this behaviour we will make a deep copy of the getter
163
+ * cf: https://reactgo.com/javascript-clone-object/
164
+ */
165
+
166
+ const allActivitiesState = JSON.parse(
167
+ JSON.stringify(this.getAllActivitiesState)
168
+ )
169
+ let size = allActivitiesState[activityID]
170
+ ? allActivitiesState[activityID].size
171
+ : 0
172
+
173
+ return size
174
+ },
175
+ createRoutes(data) {
176
+ let activity
177
+
178
+ if (data.charAt(1) == '0' || data.charAt(1) == 0) {
179
+ activity = data.substr(2)
180
+ } else {
181
+ activity = data.substr(1)
182
+ }
183
+
184
+ if (activity == 0 && activity == '0') return `introduction`
185
+ if (activity == '99') return `conclusion`
186
+ else return `activite_${activity}`
187
+ },
188
+ startActivity(a) {
189
+ //this.$bus.$emit('send-starting-event', a)
190
+ }
191
+ }
192
+ }
193
+ </script>
194
+ <style lang="scss">
195
+ .menu-card {
196
+ width: 100%;
197
+ padding: 24px;
198
+
199
+ .v-row {
200
+ margin: 0;
201
+ flex-direction: column;
202
+
203
+ .title {
204
+ flex-grow: 4;
205
+ width: 80%;
206
+ margin-bottom: 30px;
207
+
208
+ h3 {
209
+ text-align: left;
210
+ }
211
+ }
212
+
213
+ .cnt-time {
214
+ display: flex;
215
+ flex-direction: row;
216
+ align-items: center;
217
+ margin-bottom: 20px;
218
+
219
+ svg {
220
+ width: 18px;
221
+ height: 18px;
222
+ margin-right: 10px;
223
+ }
224
+ }
225
+
226
+ .box-gauge {
227
+ display: block;
228
+ width: 100%;
229
+
230
+ .box-g {
231
+ display: flex;
232
+ flex-direction: row;
233
+ align-items: center;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ </style>
@@ -156,6 +156,8 @@ export default {
156
156
  let b = null
157
157
  if (nb == 0 || nb == '0') {
158
158
  b = `introduction`
159
+ } else if (nb == 99 || nb == '99') {
160
+ b = `conclusion`
159
161
  } else {
160
162
  b = `activite_${nb}`
161
163
  }
@@ -1,155 +1,195 @@
1
- /*
2
- @ Description: Mixins to extends general fonctionnalities in a page of an activity. Add time tracker in activities
3
- @ Note: .
4
- */
5
- export const timerMixin = {
6
- data() {
7
- return {
8
- lessonTimeCounter: 0,
9
- activityTimeCounter: 0,
10
- elapsedCounter: 0, // count the elapse time since lesson started
11
- activityInterval: null,
12
- lessonInterval: null,
13
- fullInterval: null,
14
- timerState: 'stopped',
15
- elapsedInterval: null // interval of passed time since the lesson is started. used only for saving purposed
16
- }
17
- },
18
- methods: {
19
- /* Start the timer */
20
- startTimer(op) {
21
- if (op && !['activity', 'lesson'].includes(op))
22
- throw new Error('this is not a valid option for the timer')
23
- if (this.timerState === 'stopped') {
24
- switch (op) {
25
- case 'activity':
26
- this.activityInterval = setInterval(() => {
27
- this.activityTimeCounter += 1
28
- }, 1000)
29
- this.timerState = 'started'
30
-
31
- break
32
-
33
- case 'lesson':
34
- this.lessonInterval = setInterval(() => {
35
- this.lessonTimeCounter += 1
36
- }, 1000)
37
- break
38
-
39
- default:
40
- this.fullInterval = setInterval(() => {
41
- this.activityTimeCounter += 1
42
- this.lessonTimeCounter += 1
43
- }, 1000)
44
- }
45
- }
46
- },
47
- /* Stop the timer and reset thimer to zero */
48
- stopTimer(op) {
49
- if (op && !['activity', 'lesson'].includes(op))
50
- throw new Error('this is not a valid parameter')
51
-
52
- if (this.timerState === 'started') {
53
- if (op === 'activity' && this.activityTimeCounter > 0) {
54
- this.lessonTimeCounter =
55
- this.lessonTimeCounter + this.activityTimeCounter //add to the lesson counter
56
- clearInterval(this.activityInterval) //clear the interval
57
- this.activityTimeCounter = 0 // reset the counter
58
- this.timerState = 'stopped'
59
- } else if (op === 'lesson' && this.lessonTimeCounter > 0) {
60
- clearInterval(this.lessonInterval)
61
- this.lessonTimeCounter = 0
62
- } else {
63
- clearInterval(this.fullInterval)
64
- clearInterval(this.elapsedCounter)
65
- clearInterval(this.elapsedInterval)
66
-
67
- this.activityTimeCounter = 0
68
- this.lessonTimeCounter = 0
69
- this.elapsedCounter = 0
70
- }
71
- }
72
- },
73
- /* Pause the timer but does not reset to zero*/
74
- pauseTimer(op) {
75
- if (op && !['activity', 'lesson'].includes(op))
76
- throw new Error('this is not a valid parameter')
77
-
78
- if (op === 'activity' && this.activityTimeCounter > 0) {
79
- clearInterval(this.activityInterval)
80
- } else if (op === 'lesson') {
81
- clearInterval(this.lessonInterval)
82
- } else {
83
- clearInterval(this.fullInterval)
84
- clearInterval(this.elapsedInterval)
85
- }
86
- },
87
-
88
- /* Format seconds to ISO 8601 format */
89
- formatToISOString(seconds) {
90
- let d = new Date(null) //create a default date ref
91
- d.setSeconds(seconds) //set the time with passed numbers of seconds
92
- //Example of ISO 8601 time format of 24 chars: "1970-01-04T14:50:00.000Z"
93
- let ISOTime = d.toISOString()
94
-
95
- return ISOTime // YYYY-MM-DDTHH:mm:ss.sssZ
96
- },
97
-
98
- /* Parse a ISO string time period to the format of HH:mm:ss */
99
- ISOTimeParser(seconds) {
100
- let ISOTimePeriod = this.formatToISOString(seconds).substring(8, 19) // only the time portion of it
101
- ISOTimePeriod = ISOTimePeriod.split('T')
102
- const DDToHrs = (parseInt(ISOTimePeriod[0]) - 1) * 24 // convert xxT to hrs
103
- const periodOfTime = ISOTimePeriod[1] ? ISOTimePeriod[1].split(':') : null
104
-
105
- if (!periodOfTime) return '00:00:00'
106
-
107
- let timeString = ''
108
- let HH = (DDToHrs + parseInt(periodOfTime[0])).toString()
109
- let mm = periodOfTime[1]
110
- let ss = periodOfTime[2]
111
-
112
- if (HH.length === 1) HH = `0${HH}`
113
-
114
- timeString = `${HH}:${mm}:${ss}`
115
-
116
- return timeString
117
- }
118
- },
119
- beforeUnmount() {
120
- this.$bus.$off('start-timer', this.startTimer)
121
- this.$bus.$off('stop-timer', this.stopTimer)
122
- this.$bus.$off('pause-timer', this.pauseTimer)
123
- },
124
- mounted() {
125
- this.$bus.$on('start-timer', this.startTimer)
126
-
127
- this.$bus.$on('stop-timer', this.stopTimer)
128
-
129
- this.$bus.$on('pause-timer', this.pauseTimer)
130
-
131
- this.elapsedInterval = setInterval(() => {
132
- this.elapsedCounter += 1
133
- }, 1000)
134
- },
135
- computed: {
136
- activityDuration() {
137
- let duration
138
- if (this.activityTimeCounter > 0)
139
- duration = this.ISOTimeParser(this.activityTimeCounter)
140
- else duration = '00:00:00'
141
-
142
- return duration
143
- },
144
- lessonDuration() {
145
- this.lessonTimeCounter = this.lessonTimeCounter + this.activityTimeCounter
146
-
147
- let duration = this.ISOTimeParser(this.lessonTimeCounter)
148
- return duration
149
- },
150
- elapsedTime() {
151
- const eTime = this.elapsedCounter
152
- return eTime
153
- }
154
- }
155
- }
1
+ /*
2
+ @ Description: Mixins to extends general fonctionnalities in a page of an activity. Add time tracker in activities
3
+ @ Note: .
4
+ */
5
+ export const timerMixin = {
6
+ data() {
7
+ return {
8
+ lessonTimeCounter: 0,
9
+ activityTimeCounter: 0,
10
+ activityInterval: null,
11
+ lessonInterval: null,
12
+ fullInterval: null,
13
+ timerState: 'stopped',
14
+ idleCounter: 0,
15
+ idleTimeoutID: null,
16
+ idleTimeout: 5 * 60000, //5 mins
17
+ idleTimer: null
18
+ }
19
+ },
20
+ methods: {
21
+ /* Start the timer */
22
+ startTimer(op) {
23
+ if (op && !['activity', 'lesson'].includes(op))
24
+ throw new Error('this is not a valid option for the timer')
25
+ if (this.timerState == 'stopped') {
26
+ switch (op) {
27
+ case 'activity':
28
+ this.activityInterval = setInterval(() => {
29
+ this.activityTimeCounter += 1
30
+ }, 1000)
31
+ break
32
+
33
+ case 'lesson':
34
+ this.lessonInterval = setInterval(() => {
35
+ this.lessonTimeCounter += 1
36
+ }, 1000)
37
+ break
38
+
39
+ default:
40
+ this.fullInterval = setInterval(() => {
41
+ this.activityTimeCounter += 1
42
+ this.lessonTimeCounter += 1
43
+ }, 1000)
44
+ }
45
+ this.timerState = 'started'
46
+ }
47
+ },
48
+ /* Stop the timer and reset thimer to zero */
49
+ stopTimer(op) {
50
+ if (op && !['activity', 'lesson'].includes(op))
51
+ throw new Error('this is not a valid parameter')
52
+
53
+ if (this.timerState == 'started') {
54
+ if (op === 'activity' && this.activityTimeCounter > 0) {
55
+ this.lessonTimeCounter =
56
+ this.lessonTimeCounter + this.activityTimeCounter //add to the lesson counter
57
+ clearInterval(this.activityInterval) //clear the interval
58
+ this.activityTimeCounter = 0 // reset the counter
59
+ } else if (op == 'lesson' && this.lessonTimeCounter > 0) {
60
+ clearInterval(this.lessonInterval)
61
+ this.lessonTimeCounter = 0
62
+ } else {
63
+ clearInterval(this.activityInterval) //clear the interval
64
+ clearInterval(this.fullInterval)
65
+ clearInterval(this.lessonInterval)
66
+ clearInterval(this.elapsedCounter)
67
+
68
+ this.activityTimeCounter = 0
69
+ this.lessonTimeCounter = 0
70
+ this.elapsedCounter = 0
71
+ }
72
+ this.timerState = 'stopped'
73
+ }
74
+ },
75
+ /* Pause the timer but does not reset to zero*/
76
+ pauseTimer(op) {
77
+ if (op && !['activity', 'lesson'].includes(op))
78
+ throw new Error('this is not a valid parameter')
79
+
80
+ if (op === 'activity' && this.activityTimeCounter > 0) {
81
+ clearInterval(this.activityInterval)
82
+ } else if (op === 'lesson') {
83
+ clearInterval(this.lessonInterval)
84
+ } else {
85
+ clearInterval(this.fullInterval)
86
+ clearInterval(this.elapsedCounter)
87
+ }
88
+ this.timerState = 'stopped'
89
+ },
90
+ /*
91
+ * Inialize the idle Timer,
92
+ * create a timer to trick time passed since idle is start
93
+ * create a countdown to pause activity timer
94
+ */
95
+ startIdleTimer() {
96
+ this.idleTimer = setInterval(() => {
97
+ this.idleCounter += 1
98
+ }, 1000)
99
+ this.idleTimeoutID = setTimeout(
100
+ () => this.pauseTimer('activity'),
101
+ this.idleTimeout
102
+ )
103
+ },
104
+ /*
105
+ * reset idle timer .
106
+ * retart the activity counter
107
+ */
108
+ resetIdleTimer() {
109
+ this.stopIdleTimer()
110
+ if (this.timerState == 'stopped') this.startTimer('activity')
111
+ this.startIdleTimer()
112
+ },
113
+ /**
114
+ * clear idle timer
115
+ * clear idle countdown
116
+ * reset idle counter
117
+ */
118
+ stopIdleTimer() {
119
+ this.idleCounter = 0
120
+ clearInterval(this.idleTimer)
121
+ clearTimeout(this.idleTimeoutID)
122
+ },
123
+
124
+ /* Format seconds to ISO 8601 format */
125
+ formatToISOString(seconds) {
126
+ let d = new Date(null) //create a default date ref
127
+ d.setSeconds(seconds) //set the time with passed numbers of seconds
128
+ //Example of ISO 8601 time format of 24 chars: "1970-01-04T14:50:00.000Z"
129
+ let ISOTime = d.toISOString()
130
+
131
+ return ISOTime // YYYY-MM-DDTHH:mm:ss.sssZ
132
+ },
133
+
134
+ /* Parse a ISO string time period to the format of HH:mm:ss */
135
+ ISOTimeParser(seconds) {
136
+ let ISOTimePeriod = this.formatToISOString(seconds).substring(8, 19) // only the time portion of it
137
+ ISOTimePeriod = ISOTimePeriod.split('T')
138
+ const DDToHrs = (parseInt(ISOTimePeriod[0]) - 1) * 24 // convert xxT to hrs
139
+ const periodOfTime = ISOTimePeriod[1] ? ISOTimePeriod[1].split(':') : null
140
+
141
+ if (!periodOfTime) return '00:00:00'
142
+
143
+ let timeString = ''
144
+ let HH = (DDToHrs + parseInt(periodOfTime[0])).toString()
145
+ let mm = periodOfTime[1]
146
+ let ss = periodOfTime[2]
147
+
148
+ if (HH.length === 1) HH = `0${HH}`
149
+
150
+ timeString = `${HH}:${mm}:${ss}`
151
+
152
+ return timeString
153
+ }
154
+ },
155
+ beforeUnmount() {
156
+ this.$bus.$off('timer-start', this.startTimer)
157
+ this.$bus.$off('timer-tart', this.stopTimer)
158
+ this.$bus.$off('timer-pause', this.pauseTimer)
159
+ this.$bus.$off('stop-idle-timer', this.stopIdleTimer)
160
+ this.$bus.$off('start-idle-timer', this.startIdleTimer)
161
+ },
162
+ mounted() {
163
+ this.$bus.$on('timer-start', this.startTimer)
164
+ this.$bus.$on('timer-stop', this.stopTimer)
165
+ this.$bus.$on('timer-pause', this.pauseTimer)
166
+ this.$bus.$on('start-idle-timer', this.startIdleTimer)
167
+ this.$bus.$on('stop-idle-timer', this.stopIdleTimer)
168
+ },
169
+ computed: {
170
+ activityDuration() {
171
+ let duration
172
+ if (this.activityTimeCounter > 0)
173
+ duration = this.ISOTimeParser(this.activityTimeCounter)
174
+ else duration = '00:00:00'
175
+
176
+ return duration
177
+ },
178
+ lessonDuration() {
179
+ this.lessonTimeCounter = this.lessonTimeCounter + this.activityTimeCounter
180
+
181
+ let duration = this.ISOTimeParser(this.lessonTimeCounter)
182
+ return duration
183
+ },
184
+ elapsedTime() {
185
+ const eTime = this.elapsedCounter
186
+ return eTime
187
+ },
188
+ elapsedIdleTime() {
189
+ return this.idleCounter
190
+ },
191
+ timerCurrentState() {
192
+ return this.timerState
193
+ }
194
+ }
195
+ }
@@ -628,7 +628,7 @@ export const useAppStore = defineStore('$appStore', {
628
628
  this.introAcitve = status
629
629
  },
630
630
 
631
- setAppDebugMode(state, bool) {
631
+ setAppDebugMode(bool) {
632
632
  this.appDebugMode = bool
633
633
  },
634
634
 
@@ -641,7 +641,7 @@ export const useAppStore = defineStore('$appStore', {
641
641
  setDeviceType(device) {
642
642
  this.deviceType = device
643
643
  },
644
- setErrorMenu(state, error) {
644
+ setErrorMenu(error) {
645
645
  this.errMenuSetting = error
646
646
  },
647
647
  setApplicationSettings(settings) {
@@ -652,7 +652,7 @@ export const useAppStore = defineStore('$appStore', {
652
652
  this.mediaVolume = volume
653
653
  },
654
654
 
655
- setMediaMuted(state, bool) {
655
+ setMediaMuted(bool) {
656
656
  this.mediaMuted = bool
657
657
  },
658
658
 
@@ -191,7 +191,6 @@ export default class ADL {
191
191
  } else {
192
192
  userData = {}
193
193
  }
194
-
195
194
  return userData
196
195
  } catch (err) {
197
196
  console.log(err)
@@ -1726,7 +1726,6 @@ export function xapiwrapper(ADL) {
1726
1726
  * @param {boolean} strictCallbacks Callback must be executed and first param is error or null if no error
1727
1727
  * @return {Objec} containing the status of last promise request and Array of the responses;
1728
1728
  */
1729
-
1730
1729
  ADL.XHR_request = function (
1731
1730
  lrs,
1732
1731
  urls,
@@ -33,6 +33,11 @@ router.beforeResolve((to, from, next) => {
33
33
  if (from.name !== 'module')
34
34
  app.config.globalProperties.$bus.$emit('update-route-history', from)
35
35
  app.config.globalProperties.$bus.$emit('update-content', to, from, next)
36
+ //hidle detector should be stopped in menu
37
+ if (to.name == 'menu') {
38
+ app.config.globalProperties.$bus.$emit('timer-stop')
39
+ app.config.globalProperties.$bus.$emit('stop-idle-detector')
40
+ }
36
41
 
37
42
  if (noNavigationToMenu && to.name == 'menu') {
38
43
  const r = app.config.globalProperties.$helper.getRoutesFromVueRouter() //This return all the routes defined for the module