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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/{.eslintrc.js → .eslintrc.cjs} +13 -18
  2. package/bk.scss +117 -0
  3. package/package.json +23 -39
  4. package/src/$locales/en.json +30 -16
  5. package/src/$locales/fr.json +29 -16
  6. package/src/components/AppBase.vue +740 -305
  7. package/src/components/AppBaseButton.vue +33 -5
  8. package/src/components/AppBaseErrorDisplay.vue +43 -35
  9. package/src/components/AppBaseModule.vue +447 -623
  10. package/src/components/AppBasePage.vue +37 -25
  11. package/src/components/AppCompAudio.vue +266 -0
  12. package/src/components/AppCompBranchButtons.vue +52 -63
  13. package/src/components/AppCompButtonProgress.vue +1 -16
  14. package/src/components/AppCompCarousel.vue +43 -39
  15. package/src/components/AppCompInputCheckBox.vue +9 -3
  16. package/src/components/AppCompInputDropdown.vue +2 -4
  17. package/src/components/AppCompInputRadio.vue +8 -15
  18. package/src/components/AppCompInputTextTable.vue +15 -12
  19. package/src/components/AppCompInputTextToFillDropdown.vue +16 -14
  20. package/src/components/AppCompInputTextToFillText.vue +2 -2
  21. package/src/components/AppCompJauge.vue +13 -1
  22. package/src/components/AppCompMenu.vue +203 -10
  23. package/src/components/AppCompMenuItem.vue +20 -3
  24. package/src/components/AppCompNavigation.vue +351 -355
  25. package/src/components/AppCompNoteCall.vue +62 -47
  26. package/src/components/AppCompNoteCredit.vue +182 -79
  27. package/src/components/AppCompPlayBar.vue +975 -1023
  28. package/src/components/AppCompPlayBarProgress.vue +73 -0
  29. package/src/components/AppCompPopUp.vue +175 -114
  30. package/src/components/AppCompQuiz.vue +67 -81
  31. package/src/components/AppCompQuizRecall.vue +32 -5
  32. package/src/components/AppCompSVG.vue +66 -40
  33. package/src/components/AppCompSettingsMenu.vue +6 -8
  34. package/src/components/AppCompTableOfContent.vue +166 -45
  35. package/src/components/AppCompVideoPlayer.vue +154 -110
  36. package/src/components/BaseModule.vue +21 -17
  37. package/src/main.js +124 -88
  38. package/src/mixins/$mediaMixins.js +827 -0
  39. package/src/mixins/$pageMixins.js +65 -109
  40. package/src/mixins/$quizMixins.js +12 -26
  41. package/src/mixins/timerMixin.js +8 -9
  42. package/src/module/store.js +187 -68
  43. package/src/module/xapi/ADL.js +90 -53
  44. package/src/module/xapi/Crypto/Hasher.js +8 -8
  45. package/src/module/xapi/Crypto/WordArray.js +6 -6
  46. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +4 -4
  47. package/src/module/xapi/Crypto/algorithms/C_algo.js +14 -18
  48. package/src/module/xapi/Crypto/algorithms/HMAC.js +1 -1
  49. package/src/module/xapi/Crypto/algorithms/SHA1.js +1 -1
  50. package/src/module/xapi/Crypto/encoders/Base.js +7 -7
  51. package/src/module/xapi/Crypto/encoders/Base64.js +3 -3
  52. package/src/module/xapi/Crypto/encoders/Hex.js +2 -2
  53. package/src/module/xapi/Crypto/encoders/Latin1.js +3 -3
  54. package/src/module/xapi/Crypto/encoders/Utf8.js +3 -3
  55. package/src/module/xapi/Statement/index.js +1 -1
  56. package/src/module/xapi/launch.js +10 -10
  57. package/src/module/xapi/utils.js +17 -17
  58. package/src/module/xapi/wrapper.js +123 -50
  59. package/src/module/xapi/xapiStatement.js +29 -29
  60. package/src/plugins/helper.js +8 -9
  61. package/src/plugins/i18n.js +23 -10
  62. package/src/plugins/scorm.js +14 -14
  63. package/src/router/index.js +3 -4
  64. package/src/router/routes.js +10 -30
  65. package/src/shared/generalfuncs.js +31 -24
  66. package/src/shared/validators.js +730 -20
  67. package/.prettierrc.js +0 -5
  68. package/babel.config.js +0 -3
  69. package/src/components/AppBaseDragChoice.vue +0 -91
  70. package/src/components/AppBaseDropZone.vue +0 -112
  71. package/src/components/AppCompBif.vue +0 -120
  72. package/src/components/AppCompDragAndDrop.vue +0 -339
  73. package/src/components/AppCompInputAssociation.vue +0 -332
  74. package/src/components/AppCompMediaPlayer.vue +0 -397
  75. package/src/plugins/timeManager.js +0 -77
  76. package/src/routes_bckp.js +0 -313
  77. package/src/routes_static.js +0 -344
  78. package/vue.config.js +0 -83
@@ -1,37 +1,95 @@
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
4
7
  @ Must be used.
5
8
  -->
6
9
  <template>
7
10
  <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>
17
- </div>
18
- </transition>
19
-
20
- <router-view class="box" />
21
- <app-icons />
11
+ <template v-if="error.length">
12
+ <b-row>
13
+ <b-col>
14
+ <app-base-error-display
15
+ :errors-list="error"
16
+ error-type="appConfig"
17
+ error-title="Configuration de l'application"
18
+ />
19
+ </b-col>
20
+ </b-row>
21
+ </template>
22
+ <template v-else>
23
+ <template>
24
+ <transition name="bounce" mode="in-out">
25
+ <div
26
+ v-if="showBuildInfo && !buildInfoClicked"
27
+ id="build-info"
28
+ @click="buildInfoClicked = true"
29
+ >
30
+ <span>{{ getModuleInfo.courseID.toUpperCase() }}</span>
31
+ <span>FCAD {{ $helper.getFcadVersion() }}</span>
32
+ <span>{{ $helper.getBuildTime() }}</span>
33
+ </div>
34
+ </transition>
35
+
36
+ <router-view class="box" />
37
+
38
+ <app-icons />
39
+ </template>
40
+ </template>
41
+ <b-overlay
42
+ v-show="!appReady"
43
+ id="overlay_loading"
44
+ :show="!appReady"
45
+ rounded="lg"
46
+ variant="white"
47
+ blur="3px"
48
+ opacity="0.8"
49
+ >
50
+ <template #overlay>
51
+ <div class="d-flex grp-spinners align-items-center">
52
+ <b-spinner small type="grow" variant="secondary"></b-spinner>
53
+ <b-spinner
54
+ style="width: 1.2rem; height: 1.2rem"
55
+ type="grow"
56
+ variant="secondary"
57
+ ></b-spinner>
58
+
59
+ <b-spinner
60
+ style="width: 1.8rem; height: 1.8rem"
61
+ type="grow"
62
+ variant="dark"
63
+ ></b-spinner>
64
+
65
+ <b-spinner
66
+ style="width: 1.2rem; height: 1.2rem"
67
+ type="grow"
68
+ variant="secondary"
69
+ ></b-spinner>
70
+ <b-spinner small type="grow" variant="secondary"></b-spinner>
71
+ <!-- We add an SR only text for screen readers -->
72
+ <span class="sr-only">{{ $t('message.loading_state_msg') }}</span>
73
+ </div>
74
+ </template>
75
+ </b-overlay>
22
76
  </div>
23
77
  </template>
24
78
  <script>
25
79
  import { mapGetters } from 'vuex'
26
-
80
+ import { timerMixin } from '../mixins/timerMixin'
81
+ import { validateAppContent } from '../shared/validators'
82
+ import AppBaseErrorDisplay from './AppBaseErrorDisplay.vue'
83
+ import mobileDetect from 'mobile-detect'
27
84
  export default {
85
+ components: { AppBaseErrorDisplay },
86
+ mixins: [timerMixin],
28
87
  props: {
29
88
  appConfig: {
30
89
  type: Object,
31
- default: () => {
32
- return {
33
- lang: 'fr' // set language of App: default French
34
- }
90
+ required: true,
91
+ validator: (value) => {
92
+ if (import.meta.env.DEV) return validateAppContent(value).length === 0
35
93
  }
36
94
  }
37
95
  },
@@ -43,7 +101,8 @@ export default {
43
101
  initialDeviceOrentation: null,
44
102
  appIsFullScreen: false,
45
103
  resizeiPad: false,
46
- buildInfoClicked: false
104
+ buildInfoClicked: false,
105
+ error: []
47
106
  }
48
107
  },
49
108
  computed: {
@@ -52,8 +111,14 @@ export default {
52
111
  'getIsMobile',
53
112
  'getDeviceType',
54
113
  'getModuleInfo',
114
+ 'getAppStatus',
55
115
  'getUserInteraction',
56
- 'getConnectionInfo'
116
+ 'getConnectionInfo',
117
+ 'getDataFromServer',
118
+ 'getAllActivities',
119
+ 'getMediaPlaybarValues',
120
+ 'getAppConfigs',
121
+ 'getErrorMenu'
57
122
  ]),
58
123
  getwidth() {
59
124
  return window.innerWidth
@@ -61,18 +126,32 @@ export default {
61
126
  getheight() {
62
127
  return window.innerHeight
63
128
  },
129
+
64
130
  showBuildInfo() {
65
131
  return (
66
- process.env.NODE_ENV === 'production' &&
132
+ import.meta.env.PROD &&
67
133
  window.location.host === 'projets.cegepadistance.ca'
68
134
  )
135
+ },
136
+ appReady() {
137
+ let readyState = this.getAppStatus === 'ready' ? true : false
138
+
139
+ return readyState
140
+ },
141
+ displayLang() {
142
+ let lang = false
143
+ const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
144
+
145
+ if (this.getModuleInfo.packageType === 'xapi') {
146
+ lang = displayList.find((l) => l.includes(this.$i18n.locale))
147
+ }
148
+ return lang
69
149
  }
70
150
  },
71
151
  watch: {
72
152
  getConnectionInfo: {
73
153
  //in development environment (localhost), don't wait for the axios call
74
-
75
- immediate: process.env.NODE_ENV === 'development',
154
+ immediate: import.meta.env.DEV,
76
155
  handler() {
77
156
  /**
78
157
  * Attach listener to detect when user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile,
@@ -83,6 +162,7 @@ export default {
83
162
  * https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon
84
163
  */
85
164
 
165
+ if (!this.getConnectionInfo) return
86
166
  if (this.getIsMobile) {
87
167
  document.addEventListener(
88
168
  'visibilitychange',
@@ -93,22 +173,6 @@ export default {
93
173
  },
94
174
  false
95
175
  )
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
- )
112
176
  }
113
177
  // initialize scorm Commuinication with LMS || xAPI LRS
114
178
  if (this.getModuleInfo.packageType === 'scorm') this.$scorm.Initialize()
@@ -132,20 +196,34 @@ export default {
132
196
  }
133
197
 
134
198
  this.$xapi._configLRS(config) // configure and launch LRS
135
- this.fetchSettingsFromServer()
136
- }
199
+
200
+ setTimeout(() => {
201
+ this.fetchDataFromServer().then(() => {
202
+ setTimeout(() => {
203
+ this.setProgress()
204
+ }, 200)
205
+ })
206
+ }, 200)
207
+ } else this.setProgress()
137
208
  }
138
209
  }
139
210
  },
140
211
 
141
212
  created() {
142
- this.setProgress()
213
+ if (import.meta.env.DEV) {
214
+ this.checkForErrors()
215
+ }
216
+ //Declare loading state if no error was detected
217
+
218
+ if (!this.error.length) {
219
+ this.updateTracker('appBase', 'loading')
220
+ this.$store.dispatch('setAppConfigs', this.appConfig)
221
+ }
222
+
143
223
  window.versionFCAD = this.$helper.getFcadVersionString()
144
224
  //check if this is running in a mobile environment and register state in the store
145
- const mobileDetect = require('mobile-detect')
146
-
147
225
  const md = new mobileDetect(window.navigator.userAgent)
148
- md.mobile()
226
+ md.mobile() !== null
149
227
  ? this.$store.dispatch('setMobileState', true)
150
228
  : this.$store.dispatch('setMobileState', false)
151
229
  const currentBrowser = this.getBrowser() //get current browser vendor
@@ -155,25 +233,44 @@ export default {
155
233
 
156
234
  // register device type running the App in the store (ios, Android or Deskop)
157
235
  this.$store.dispatch('setDeviceType', this.detectDevice())
158
- this.fetchSettingsFromServer()
236
+
237
+ this.$bus.$on('set-comp-status', this.updateTracker)
238
+ this.$bus.$on('send-xapi-statement', this.sendXapiStatements)
239
+ this.$bus.$on('reset-userdata', this.resetUserData)
240
+ this.$bus.$on('fire-exit-event', this.endLesson)
241
+ this.$bus.$on('reset-focus-on', this.resetFocus)
242
+ this.$bus.$on('move-to-target', this.moveTo)
159
243
  },
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)
244
+ beforeMount() {
245
+ window.addEventListener(
246
+ 'beforeunload',
247
+ (e) => {
248
+ this.executeCloseEventTriggered()
249
+ // e.preventDefault()
250
+ // e.returnValue = true
251
+ },
252
+ true
165
253
  )
254
+ },
166
255
 
167
- this.$bus.$on('set-user-progress', this.setProgress())
256
+ mounted() {
257
+ // set the language of the app
258
+ this.setLocale(this.getAppConfigs.lang)
168
259
  },
169
- beforeDestroy() {
170
- this.$bus.$off('set-comp-status')
260
+ beforeUnmount() {
261
+ this.$bus.$off('set-comp-status', this.updateTracker)
262
+ this.$bus.$off('reset-userdata', this.resetUserData)
263
+ this.$bus.$off('fire-exit-event', this.endLesson)
264
+ this.$bus.$off('reset-focus-on', this.resetFocus)
265
+ this.$bus.$off('send-xapi-statement', this.sendXapiStatements)
266
+ this.$bus.$off('move-to-target', this.moveTo)
171
267
  },
172
268
  methods: {
173
269
  /**
174
270
  * @description set the desired language for the app default is french
175
271
  * @param {String} [lang=fr]
176
272
  */
273
+
177
274
  setLocale(lang) {
178
275
  if (!lang) lang = 'fr'
179
276
  else {
@@ -185,9 +282,11 @@ export default {
185
282
  this.$i18n.locale = lang
186
283
  }
187
284
  },
285
+
188
286
  getScormState() {
189
287
  return this.$scorm.GetValue('cmi.suspend_data')
190
288
  },
289
+
191
290
  getBrowser() {
192
291
  let browser
193
292
  const test = (regexp) => regexp.test(window.navigator.userAgent) //defining the testing fonction
@@ -241,41 +340,59 @@ export default {
241
340
  return device
242
341
  },
243
342
 
244
- fetchSettingsFromServer() {
343
+ async fetchDataFromServer() {
344
+ this.updateTracker('appBase_fetch', 'loading')
245
345
  if (this.getModuleInfo.packageType !== 'xapi') return
246
346
  if (!this.getConnectionInfo || !this.getConnectionInfo.remote) return
247
347
 
248
- const activity_id = this.getModuleInfo.courseID
249
- ? this.getConnectionInfo.activity_id.replace(
250
- `/${this.getModuleInfo.id}`,
251
- ''
252
- )
253
- : this.getConnectionInfo.activity_id
348
+ const progress = await this.$xapi._getProgress(
349
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
350
+ this.getConnectionInfo.activity_id
351
+ )
352
+ const { routeHistory, ...userProgress } = progress
254
353
 
255
- //Get existing record for user preferred settings
256
- const applicationSettings = this.$xapi._getPreferredSettings(
354
+ const completedState = await this.$xapi._getLessonStatus(
257
355
  this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
258
- activity_id
356
+ this.getConnectionInfo.activity_id
259
357
  )
260
358
 
261
- if (applicationSettings.length && applicationSettings[0].userSettings)
262
- //save result to store
263
- this.$store.dispatch(
264
- 'setApplicationSettings',
265
- applicationSettings[0].userSettings
266
- )
359
+ //=======UPCOMING: Uncomment lines bellow when these properties are integrated ========
360
+ // Get existing record for user preferred settings
361
+ // const applicationSettings = await this.$xapi._getPreferredSettings(
362
+ // this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
363
+ // this.getConnectionInfo.activity_id
364
+ // )
267
365
 
268
- //Get existing record for play bar settings
269
- const pbValues = this.$xapi._getPlaybarValues(
366
+ //======= End UPCOMING: Uncomment when this property are integrated ========
367
+ //Get existing record for play bar settings. Play bar info is from the activity Parent ID and not activity id
368
+ const _url = new URL(this.getConnectionInfo.activity_id)
369
+ const parentID = `${_url.origin}/${this.getModuleInfo.courseID}` // redefining activity id for statement
370
+ const playbarValues = await this.$xapi._getPlaybarValues(
371
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
372
+ parentID
373
+ )
374
+ const lessonPosition = await this.$xapi._getLessonPosition(
270
375
  this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
271
- activity_id
376
+ this.getConnectionInfo.activity_id
272
377
  )
273
378
 
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
- }
379
+ //Update the App Store data
380
+ this.$store.dispatch('updateDataFetchFromServer', {
381
+ userProgress,
382
+ routeHistory,
383
+ lessonPosition,
384
+ completedState,
385
+ playbarValues
386
+ })
387
+ // console.log('DATA FROM SERVER', {
388
+ // userProgress,
389
+ // routeHistory,
390
+ // lessonPosition,
391
+ // completedState,
392
+ // playbarValues
393
+ // })
394
+
395
+ this.updateTracker('appBase_fetch', 'ready')
279
396
  },
280
397
 
281
398
  executeCloseEventTriggered() {
@@ -293,13 +410,11 @@ export default {
293
410
  this.getConnectionInfo.actor &&
294
411
  this.getConnectionInfo.remote
295
412
  ) {
296
- // Communicate ending of activity
297
- this.$bus.$emit('fire-exit-event', null, true)
298
- this.routeChangeCounter = 0 //reset counter after saving
413
+ this.endLesson(null, true)
299
414
  }
300
415
  },
301
- setProgress() {
302
- this.updateTracker('appBase', 'loading')
416
+
417
+ async setProgress() {
303
418
  const packageType = this.getModuleInfo.packageType
304
419
 
305
420
  switch (packageType) {
@@ -317,7 +432,6 @@ export default {
317
432
 
318
433
  this.$store.dispatch('setUserMetaData', userProgress)
319
434
  this.$store.dispatch('setRouteHistory', routeHistory) // update store recored with existing record
320
- this.updateTracker('appBase', 'ready')
321
435
  }
322
436
 
323
437
  // No LMS use LocalStorage record
@@ -325,16 +439,12 @@ export default {
325
439
  this.$idb.openDB().then(() => {
326
440
  this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
327
441
  if (res && res.$record) {
328
- const {
329
- routeHistory,
330
- userSettings,
331
- ...userData
332
- } = res.$record
442
+ const { routeHistory, userSettings, ...userData } =
443
+ res.$record
333
444
  this.$store.dispatch('setUserMetaData', userData) // update store record with existing record
334
445
  this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing route info
335
446
  if (userSettings)
336
447
  this.$store.dispatch('setApplicationSettings', userSettings) // update store record with existing user settings
337
- this.updateTracker('appBase', 'ready')
338
448
  }
339
449
  })
340
450
  })
@@ -349,266 +459,591 @@ export default {
349
459
  this.getConnectionInfo.actor &&
350
460
  this.getConnectionInfo.remote
351
461
  ) {
462
+ if (!this.getDataFromServer) return
352
463
  //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')
464
+ const { playbarValues, routeHistory, userProgress } =
465
+ this.getDataFromServer
466
+
467
+ this.$store.dispatch('setUserMetaData', userProgress) // update store record with existing record
468
+ this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing record
469
+ if (playbarValues)
470
+ this.$store.dispatch('setMediaPLaybarValues', playbarValues) // update store record with existing record
471
+ //Should update the playbar values
367
472
  } else {
368
473
  //Get existing records for user data in local store
369
474
  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
- }
475
+ this.$idb.getFromDB(this.getModuleInfo.idbID).then((res) => {
476
+ if (res && res.$record) {
477
+ const { routeHistory, userSettings, ...userData } =
478
+ res.$record
479
+
480
+ this.$store.dispatch('setUserMetaData', userData) // update store record with existing record
481
+ this.$store.dispatch('setRouteHistory', routeHistory) // update store record with existing route info
482
+
483
+ if (userSettings) {
484
+ this.$store.dispatch(
485
+ 'setApplicationSettings',
486
+ userSettings
487
+ ) // update store record with existing user setting
389
488
  }
390
- })
391
- .then(() => this.updateTracker('appBase', 'ready'))
489
+ }
490
+ })
392
491
  })
393
492
  }
394
493
  }
395
494
 
396
495
  break
397
496
  }
497
+
498
+ this.updateTracker('appBase', 'ready')
398
499
  },
399
500
  updateTracker(name, status) {
400
501
  this.$store.dispatch('updateCompStatusTracker', { name, status })
401
- }
402
- }
403
- }
404
- </script>
405
- <style lang="scss">
406
- #App-base {
407
- width: 100%;
408
- height: 100vh;
502
+ },
503
+ //============================Multiple Satements Sending at once======================================
504
+ /**
505
+ * @description Send a custom statements to the lrs. Receive some custom params and build a statement to send
506
+ * @param {Array} stmts- array of stamtents objects that will be send to the server
507
+ * @param {Function} cb
508
+ */
509
+ async sendXapiStatements(stmts, cb = null, withFetch = true) {
510
+ cb = cb || null
511
+ if (!stmts) return
512
+ let stmtsArray = []
409
513
 
410
- #build-info {
411
- opacity: 0.9;
412
- position: fixed;
413
- right: 0;
414
- bottom: 0;
415
- color: #fff;
416
- text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4);
417
- background-color: hotpink;
418
- font-family: serif, Impact, Arial;
419
- font-size: 11px;
420
- padding: 3px;
421
- cursor: pointer;
422
- z-index: 999;
423
- span {
424
- text-align: center;
425
- display: block;
426
- }
427
- }
514
+ stmts.constructor === Object
515
+ ? stmtsArray.push(stmts)
516
+ : (stmtsArray = [...stmts])
428
517
 
429
- .module {
430
- width: 100%;
431
- height: 100%;
432
- 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
- }
518
+ const crsParams = this.getModuleInfo
519
+
520
+ if (
521
+ this.getConnectionInfo &&
522
+ this.getConnectionInfo.actor &&
523
+ this.getConnectionInfo.remote
524
+ ) {
525
+ const stmtsQueue = []
449
526
 
450
- .module-wrapper {
451
- width: 100%;
452
- position: relative;
527
+ stmtsArray.forEach((stmtObj) => {
528
+ const {
529
+ id,
530
+ result = null,
531
+ definition,
532
+ objectType,
533
+ type,
534
+ description,
535
+ verb,
536
+ extensions,
537
+ duration,
538
+ completion
539
+ } = stmtObj
453
540
 
454
- #wrapper-content {
455
- width: 100%;
456
- position: relative;
457
- padding-right: 0 !important;
458
- padding-left: 0 !important;
541
+ //define the activity id
542
+ let activityId
543
+ //=========================== activity ID of Object ==========================================
544
+ /*
545
+ * Define the acitivity id of the stmt according to following:
546
+ * The statement is sent at component/ element level : there is Id and Id is not null (id of the element)
547
+ * The statement is sent at module level. there is no id
548
+ * the Statement must be sent at the course level: There course_id exist and id is the course_id
549
+ */
459
550
 
460
- &.active {
461
- overflow: hidden;
462
- }
551
+ if (id && id !== crsParams.courseID)
552
+ activityId = `${this.getConnectionInfo.activity_id}/${id}`
553
+ else if (crsParams.courseID && id === crsParams.courseID)
554
+ activityId = this.getConnectionInfo.activity_id.replace(
555
+ `/${crsParams.id}`,
556
+ ''
557
+ )
558
+ else activityId = `${this.getConnectionInfo.activity_id}`
559
+
560
+ //define the statement object
561
+ let stmt = {
562
+ actor: this.getConnectionInfo.actor,
563
+ verb: (() => {
564
+ if (verb && this.$xapi.verbs[verb.trim()])
565
+ return this.$xapi.verbs[verb.trim()]
566
+ else {
567
+ /**
568
+ * Determine the verb to use by:
569
+ * Checking if the activity is already included in the data fetch from server.
570
+ * If not, fetch directly from the server
571
+ * There is a found, verb is 'RESUMED'
572
+ * There is no found, verb is 'INITIALIZED'
573
+ */
463
574
 
464
- .box {
465
- width: 100%;
466
- height: 95%;
575
+ //Regex to test that id contains list of word
576
+ const regex =
577
+ /(menu|activite_(\d)*|A(\d){2}|conclusion|introduction)$/gm
467
578
 
468
- .app-page {
469
- width: 100%;
470
- margin-top: 60px;
579
+ switch (true) {
580
+ case regex.test(activityId): {
581
+ //ID is of activity (menu, conclusion, activite, intro )
582
+ const activityStr = activityId.split('/').toReversed()[0]
471
583
 
472
- .row {
473
- width: 100%;
474
- margin-right: 0;
475
- margin-left: 0;
584
+ if (!this.getDataFromServer)
585
+ return this.$xapi.verbs.initialized
586
+ const { userProgress } = this.getDataFromServer
587
+ let activity_ref
588
+ //Define the activity reference
589
+ if (['menu', 'introduction', 'A00'].includes(activityStr))
590
+ activity_ref = 'A00'
591
+ else if (['conclusion', 'A99'].includes(activityStr))
592
+ activity_ref = 'A99'
593
+ else if (activityStr.includes('activite_')) {
594
+ const aNum = activityStr.split('_')[1]
595
+ activity_ref = aNum.length > 1 ? `A${aNum}` : `A0${aNum}`
596
+ } else activity_ref = activityStr // should be AXX
597
+
598
+ if (userProgress && userProgress[activity_ref])
599
+ return this.$xapi.verbs.resumed
600
+ else return this.$xapi.verbs.initialized
601
+ }
602
+ default: {
603
+ //ID his of Lesson or doesn't exist relate (menu, conclusion, activite, intro )
604
+ if (
605
+ this.$xapi._getAgent(
606
+ this.getConnectionInfo.actor.mbox.replace(
607
+ 'mailto:',
608
+ ''
609
+ ),
610
+ activityId
611
+ )
612
+ )
613
+ return this.$xapi.verbs.resumed
614
+ else return this.$xapi.verbs.initialized
615
+ }
616
+ }
617
+ }
618
+ })(),
619
+ object: {
620
+ id: activityId,
621
+ definition: {
622
+ name: {
623
+ [this.displayLang]: `${definition}`
624
+ },
625
+ description: {
626
+ [this.displayLang]: `${description}`,
627
+ type: type || 'http://activitystrea.ms/schema/1.0/page'
628
+ }
629
+ },
630
+ objectType: objectType || 'Activity'
631
+ },
632
+ context: {
633
+ contextActivities: {}
476
634
  }
477
635
  }
478
- }
636
+ //===================== contextActivity parent =====================//
637
+ /*
638
+ Define parent in the contextActivity
639
+ When:
640
+ 1- when we have the id (parent === module)
641
+ 2- when with have no id but we have a course_id (parent is cours_id)
642
+ */
643
+ const activityParent = (() => {
644
+ if (crsParams.courseID && !id)
645
+ return {
646
+ id: this.getConnectionInfo.activity_id.replace(
647
+ `/${crsParams.id}`,
648
+ ''
649
+ ),
650
+ objectType: objectType || 'Activity'
651
+ }
652
+ else if (id && id !== crsParams.courseID)
653
+ return {
654
+ id: `${this.getConnectionInfo.activity_id}`,
655
+ objectType: objectType || 'Activity'
656
+ }
657
+ else return null
658
+ })()
659
+
660
+ // Add the parent key of the context activity
661
+ if (activityParent)
662
+ stmt.context.contextActivities['parent'] = [activityParent]
663
+
664
+ //===================== contextActivity grouping =====================//
665
+
666
+ //Defining the Grouping of the context activity
667
+ const activityGrouping = (() => {
668
+ if (
669
+ activityParent &&
670
+ crsParams.courseID &&
671
+ activityParent.id.includes(crsParams.id)
672
+ )
673
+ return {
674
+ id: this.getConnectionInfo.activity_id.replace(
675
+ `/${crsParams.id}`,
676
+ ''
677
+ )
678
+ }
679
+ else return null
680
+ })()
681
+ //Adding the grouping of the context activity
682
+ if (activityGrouping)
683
+ stmt.context.contextActivities['grouping'] = [activityGrouping]
684
+
685
+ //add result data to statement
686
+ if (result) stmt['result'] = result
687
+
688
+ // Add duration info to statement when activity is complete
689
+ if (['completed', 'suspended', 'terminated'].includes(verb)) {
690
+ if (!stmt.result) stmt.result = {} // check if exist
691
+
692
+ let d
693
+
694
+ duration
695
+ ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
696
+ duration.split(':')[2]
697
+ }S`)
698
+ : (d = `PT${this.activityDuration.split(':')[0]}H${
699
+ this.activityDuration.split(':')[1]
700
+ }M${this.activityDuration.split(':')[2]}S`)
701
+
702
+ stmt.result['duration'] = d
703
+ //Set the completion status of the result
704
+ if (completion) stmt.result['completion'] = completion
705
+ }
706
+
707
+ // ===================== Extension of the Object definition =====================//
708
+ if (
709
+ extensions &&
710
+ extensions.constructor === Array &&
711
+ extensions.length > 0
712
+ ) {
713
+ //Validate each entry given in the extension Array
714
+ extensions.forEach((e) => {
715
+ //entry must be of type Object
716
+ if (e.constructor !== Object)
717
+ throw new Error(`'${e}' is not a valid value. Must be Object`)
718
+
719
+ //Entry Must have id and content keys
720
+ const validKey = ['id', 'content']
721
+ Object.keys(e).forEach((key) => {
722
+ if (!validKey.includes(key))
723
+ throw new Error(`Not valid key '${key}' for entry ${e}`)
724
+
725
+ //id must be a String
726
+ if (key === 'id' && e[key] && e[key].constructor !== String)
727
+ throw new Error(`'${key}' must be of type String`)
728
+ })
729
+
730
+ stmt.object.definition['extensions'] = {
731
+ ...stmt.object.definition['extensions'],
732
+ [`${this.getConnectionInfo.activity_id}/${e.id}`]: e.content
733
+ }
734
+ })
735
+ }
736
+
737
+ //============================================================
738
+ stmtsQueue.push(stmt)
739
+ })
740
+
741
+ this.$xapi._sendStatements(stmtsQueue, cb, withFetch)
479
742
  }
743
+ },
480
744
 
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;
745
+ /**
746
+ * @param {Function} cb
747
+ * @param {Bool} option - set if must use Fetch API or not. default = false
748
+ */
749
+ endLesson(cb, option = true) {
750
+ cb = cb || null
751
+ let text
752
+ //Defining the text to display for stmt description and definition
753
+ switch (this.$i18n.locale) {
754
+ case 'fr':
755
+ if (this.getModuleInfo.courseID)
756
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
757
+ else text = `Le ${this.getModuleInfo.id}`
758
+ break
759
+ case 'en':
760
+ if (this.getModuleInfo.courseID)
761
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
762
+ else text = `The ${this.getModuleInfo.id}`
763
+ break
490
764
  }
491
- }
492
765
 
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;
766
+ // Retrieve only user data in lessons front its interaction that we want to send to the LRS.
767
+ // Note: User Settings are sent on a different URI and statement
768
+ const { isFistTime, userSettings, ...lessonsData } =
769
+ this.getUserInteraction
770
+
771
+ const stmt = {
772
+ verb: 'suspended',
773
+ definition: text,
774
+ description: text,
775
+ extensions: [
776
+ {
777
+ id: 'ending-point',
778
+ content: this.$route.name
779
+ },
780
+ {
781
+ id: 'user-data',
782
+ content: {
783
+ routeHistory: this.getRouteHistory,
784
+ ...lessonsData
523
785
  }
786
+ }
787
+ ],
788
+ duration: this.lessonDuration
789
+ }
524
790
 
525
- #progress-shaddow {
526
- display: block;
527
- height: 100%;
528
- position: absolute;
529
- top: 0px;
530
- left: 0px;
531
- z-index: -1;
791
+ const endStmt = { ...stmt }
792
+ endStmt.verb = 'terminated'
793
+ //================================STATEMENT FOR THE PLAYBAR ===============================================
794
+ //Creating custom statement
795
+ const stmtPlaybar = {
796
+ id: (() => {
797
+ if (this.getModuleInfo.courseID) return this.getModuleInfo.courseID
798
+ else return null
799
+ })(),
800
+ verb: 'played',
801
+ definition: text,
802
+ description: text,
803
+ extensions: [
804
+ {
805
+ id: 'playbar-values',
806
+ content: {
807
+ ...this.getMediaPlaybarValues()
532
808
  }
533
809
  }
534
- }
810
+ ]
811
+ }
812
+ //================================STATEMENT FOR THE PLAYBAR ===============================================
535
813
 
536
- .ctrl-subtitle {
537
- //width: $widthCtrSubtitle;
538
- width: 10%;
539
- }
814
+ this.sendXapiStatements([stmtPlaybar, stmt, endStmt], null, option) //send xapi statement
540
815
 
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
- }
816
+ if (this.timerState === 'started') setTimeout(() => this.stopTimer(), 0) //clear the timer
817
+
818
+ if (cb) cb()
819
+ },
820
+
821
+ /** @description Method to reset the progression status of user
822
+ * the function reset first the user data in the app store then
823
+ * reset the idbStore if APP is running locally or prepare a statement with new userData Object to be sent over the LRS
824
+ * for the connected userAgent, activity if APP is Running online
825
+ * @event 'send-xapi-statement' emit to AppBaseModule.vue that will be executed by sendXapiStatement(s)
826
+ */
827
+ async resetUserData() {
828
+ this.$bus.$emit('set-comp-status', 'appBase', 'loading')
829
+ this.$store.dispatch('setUserMetaData', {}) // resetting store record with existing for user data
830
+ this.$store.dispatch('setRouteHistory', []) // resetting store record for all last visited pages
831
+
832
+ this.$bus.$emit('stop-timer')
833
+ if (this.getModuleInfo.packageType !== 'xapi') return
834
+
835
+ if (!this.getConnectionInfo || this.getConnectionInfo.remote == false)
836
+ return this.$idb.deleteDataInDB(this.getModuleInfo.idbID) //Must call the idb delete methode to reset indexDB store
837
+
838
+ //Send a completion statement for the current activity
839
+ let aTitle = `${this.$t('text.activity')} ${
840
+ this.getConnectionInfo.activity_id
841
+ }`
842
+
843
+ //Custom text for description and definition of the stament to be sent
844
+ let text
845
+ this.$i18n.locale == 'fr'
846
+ ? (text = `L'${aTitle} de ${this.getModuleInfo.id}`)
847
+ : (text = `The ${aTitle} of ${this.getModuleInfo.id}`)
848
+
849
+ /*Dispatch a send statement event to method AppBaseModule sendXapiStatement(s)
850
+ Content values are:
851
+ User data URI: "host_address/course_ID/Lesson_ID/user-data" ex: "http://localhost:8080/330N01FD6001/m1l1/ // user-data" and
852
+ User Bookmark URI: "host_address/course_ID/Lesson_ID/ending-point" ex: ""http://localhost:8080/330N01FD6001/m1l1/ending-point": "menu"
853
+ */
854
+ const baseStmt = {
855
+ definition: text,
856
+ description: text
857
+ }
858
+ const exitStmt = {
859
+ ...baseStmt,
860
+ verb: 'exited'
550
861
  }
862
+
863
+ const endStmt = {
864
+ ...baseStmt,
865
+ verb: 'terminated',
866
+ extensions: [
867
+ {
868
+ id: 'ending-point',
869
+ content: this.$route.name
870
+ },
871
+ {
872
+ id: 'user-data',
873
+ content: new Object()
874
+ }
875
+ ],
876
+ duration: null,
877
+ completion: false // Resetting completion state value to
878
+ }
879
+
880
+ this.sendXapiStatements([exitStmt, endStmt])
881
+
882
+ this.$bus.$emit('set-comp-status', 'appBase', 'ready')
883
+
884
+ // Creating a asynchronous fecth method that would resolve after a 2 second
885
+ const fetchData = async () => {
886
+ return new Promise((resolve) => {
887
+ setTimeout(
888
+ () =>
889
+ resolve(
890
+ this.$xapi._getProgress(
891
+ this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
892
+ this.getConnectionInfo.activity_id
893
+ )
894
+ ),
895
+ 2000
896
+ )
897
+ })
898
+ }
899
+
900
+ // Make request over user current data after sending statement to assure that the resetting worked
901
+ const usrData = await fetchData()
902
+ this.$bus.$emit('reset-complete')
903
+ console.log('😄 USER STATUS: ', {
904
+ User: this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
905
+ ActivityID: this.getConnectionInfo.activity_id,
906
+ Record: usrData
907
+ })
908
+ },
909
+
910
+ /**
911
+ * @description- Scroll directly to the target* containt
912
+ * target content can be any Node defined by the ID
913
+ * @param {String} target = id of HTMLElement. if target don't existe will scroll to wrapper-content
914
+ * @param {Obj} opt = scroll behavior to apply default { top: 0, left: 0, behavior: 'auto' }
915
+ */
916
+
917
+ moveTo(target, opt = { top: 0, left: 0, behavior: 'auto' }) {
918
+ if (target.constructor !== String)
919
+ throw new Error(
920
+ '⚠️ Not supported value for @target. Must be of type String'
921
+ )
922
+
923
+ let skipTo = document.querySelector(`#wrapper-content`) //default definition of main element
924
+
925
+ if (target) {
926
+ let targetEl = document.querySelector(`#${target}`) // search for node element specified as main
927
+
928
+ if (targetEl) skipTo = targetEl
929
+ }
930
+
931
+ opt.top = skipTo.offsetTop + opt.top // Set scroll top from target top
932
+
933
+ skipTo.setAttribute('tabIndex', -1) //Allowing accessibility control with keyboard
934
+ window.scrollTo(opt)
935
+ this.resetFocus(skipTo) //focus on the target
936
+ },
937
+
938
+ /**
939
+ * @description Reset the focus the element
940
+ * @param {HTMLElement} e - element that will get focus
941
+ */
942
+ resetFocus(e) {
943
+ if (e) e.focus()
944
+ },
945
+ /**
946
+ * @description Mothods to validate that application settings are correctly
947
+ * Opens error component when configurations are not correct
948
+ *
949
+ */
950
+ checkForErrors() {
951
+ let errMessage
952
+
953
+ if (validateAppContent(this.appConfig, true).length) {
954
+ return (this.error = validateAppContent(this.appConfig, true))
955
+ }
956
+ const { list: activitiesList } = this.getAllActivities()
957
+ let { no_menu, is_single_activity } = this.appConfig
958
+
959
+ let noMenu = no_menu == undefined ? false : no_menu
960
+
961
+ let isSingleActivity =
962
+ is_single_activity == undefined ? false : is_single_activity
963
+
964
+ let err
965
+ if (this.getErrorMenu) err = true
966
+ else err = false
967
+
968
+ let consoleMsg = ''
969
+ //error if There is more than one activity when is_single_activity
970
+ switch (true) {
971
+ case isSingleActivity && activitiesList.size > 1:
972
+ 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`
973
+
974
+ consoleMsg = `Cannot have more than 1 activity with this settings configuration. Either disable 💲is_single_activity or DELETE ALL others activities`
975
+ this.error.push(errMessage)
976
+ break
977
+
978
+ case isSingleActivity && !noMenu:
979
+ 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>`
980
+ consoleMsg = `Cannot have MENU with current settings configuration. Either set 💲no_menu:false or 💲is_single_activity:false`
981
+ this.error.push(errMessage)
982
+ break
983
+
984
+ case err:
985
+ 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 `
986
+ consoleMsg = this.getErrorMenu
987
+ this.error.push(errMessage)
988
+ break
989
+ }
990
+
991
+ if (errMessage)
992
+ return console.warn(
993
+ `%c WARNING!>>> ${consoleMsg}`,
994
+ 'background: orange; color: white; display: block; border-radius:5px; margin:5px;'
995
+ )
551
996
  }
552
997
  }
553
998
  }
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;
999
+ </script>
1000
+ <style lang="scss">
1001
+ body {
1002
+ &:focus {
1003
+ border: none !important;
568
1004
  }
1005
+ }
1006
+ #App-base {
1007
+ width: 100%;
1008
+ height: 100%;
1009
+ min-height: 100vh;
569
1010
 
570
- .macWarn-box {
571
- width: 50%;
572
- position: absolute;
573
-
574
- top: calc(45% - 15% / 2);
575
- left: 25%;
1011
+ #overlay_loading {
1012
+ position: fixed !important;
1013
+ width: 100%;
1014
+ height: 100%;
1015
+ top: 0;
1016
+ left: 0;
1017
+ z-index: 9999;
576
1018
 
577
- h1 {
578
- text-align: center;
579
- color: #fff !important;
1019
+ .grp-spinners span {
1020
+ margin: 0px 2px;
580
1021
  }
1022
+ }
581
1023
 
582
- p {
1024
+ #build-info {
1025
+ opacity: 0.9;
1026
+ position: fixed;
1027
+ right: 0;
1028
+ bottom: 0;
1029
+ color: #fff;
1030
+ text-shadow: -1px 1px 1px rgba(0, 0, 0, 0.4);
1031
+ background-color: hotpink;
1032
+ font-family: serif, Impact, Arial;
1033
+ font-size: 11px;
1034
+ padding: 3px;
1035
+ cursor: pointer;
1036
+ z-index: 999;
1037
+ span {
583
1038
  text-align: center;
584
- font-size: 1.3rem;
585
- color: #fff !important;
586
- }
587
-
588
- #btn-warning-ok {
589
1039
  display: block;
590
- margin: 10px auto;
591
1040
  }
592
1041
  }
593
- }
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;
609
- }
610
1042
 
611
- .svg-hidden {
612
- display: none;
1043
+ .box {
1044
+ width: 100%;
1045
+ height: 100%;
1046
+ position: relative;
1047
+ }
613
1048
  }
614
1049
  </style>