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.
- package/{.eslintrc.js → .eslintrc.cjs} +13 -18
- package/bk.scss +117 -0
- package/package.json +23 -39
- package/src/$locales/en.json +30 -16
- package/src/$locales/fr.json +29 -16
- package/src/components/AppBase.vue +740 -305
- package/src/components/AppBaseButton.vue +33 -5
- package/src/components/AppBaseErrorDisplay.vue +43 -35
- package/src/components/AppBaseModule.vue +447 -623
- package/src/components/AppBasePage.vue +37 -25
- package/src/components/AppCompAudio.vue +266 -0
- package/src/components/AppCompBranchButtons.vue +52 -63
- package/src/components/AppCompButtonProgress.vue +1 -16
- package/src/components/AppCompCarousel.vue +43 -39
- package/src/components/AppCompInputCheckBox.vue +9 -3
- package/src/components/AppCompInputDropdown.vue +2 -4
- package/src/components/AppCompInputRadio.vue +8 -15
- package/src/components/AppCompInputTextTable.vue +15 -12
- package/src/components/AppCompInputTextToFillDropdown.vue +16 -14
- package/src/components/AppCompInputTextToFillText.vue +2 -2
- package/src/components/AppCompJauge.vue +13 -1
- package/src/components/AppCompMenu.vue +203 -10
- package/src/components/AppCompMenuItem.vue +20 -3
- package/src/components/AppCompNavigation.vue +351 -355
- package/src/components/AppCompNoteCall.vue +62 -47
- package/src/components/AppCompNoteCredit.vue +182 -79
- package/src/components/AppCompPlayBar.vue +975 -1023
- package/src/components/AppCompPlayBarProgress.vue +73 -0
- package/src/components/AppCompPopUp.vue +175 -114
- package/src/components/AppCompQuiz.vue +67 -81
- package/src/components/AppCompQuizRecall.vue +32 -5
- package/src/components/AppCompSVG.vue +66 -40
- package/src/components/AppCompSettingsMenu.vue +6 -8
- package/src/components/AppCompTableOfContent.vue +166 -45
- package/src/components/AppCompVideoPlayer.vue +154 -110
- package/src/components/BaseModule.vue +21 -17
- package/src/main.js +124 -88
- package/src/mixins/$mediaMixins.js +827 -0
- package/src/mixins/$pageMixins.js +65 -109
- package/src/mixins/$quizMixins.js +12 -26
- package/src/mixins/timerMixin.js +8 -9
- package/src/module/store.js +187 -68
- package/src/module/xapi/ADL.js +90 -53
- package/src/module/xapi/Crypto/Hasher.js +8 -8
- package/src/module/xapi/Crypto/WordArray.js +6 -6
- package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +4 -4
- package/src/module/xapi/Crypto/algorithms/C_algo.js +14 -18
- package/src/module/xapi/Crypto/algorithms/HMAC.js +1 -1
- package/src/module/xapi/Crypto/algorithms/SHA1.js +1 -1
- package/src/module/xapi/Crypto/encoders/Base.js +7 -7
- package/src/module/xapi/Crypto/encoders/Base64.js +3 -3
- package/src/module/xapi/Crypto/encoders/Hex.js +2 -2
- package/src/module/xapi/Crypto/encoders/Latin1.js +3 -3
- package/src/module/xapi/Crypto/encoders/Utf8.js +3 -3
- package/src/module/xapi/Statement/index.js +1 -1
- package/src/module/xapi/launch.js +10 -10
- package/src/module/xapi/utils.js +17 -17
- package/src/module/xapi/wrapper.js +123 -50
- package/src/module/xapi/xapiStatement.js +29 -29
- package/src/plugins/helper.js +8 -9
- package/src/plugins/i18n.js +23 -10
- package/src/plugins/scorm.js +14 -14
- package/src/router/index.js +3 -4
- package/src/router/routes.js +10 -30
- package/src/shared/generalfuncs.js +31 -24
- package/src/shared/validators.js +730 -20
- package/.prettierrc.js +0 -5
- package/babel.config.js +0 -3
- package/src/components/AppBaseDragChoice.vue +0 -91
- package/src/components/AppBaseDropZone.vue +0 -112
- package/src/components/AppCompBif.vue +0 -120
- package/src/components/AppCompDragAndDrop.vue +0 -339
- package/src/components/AppCompInputAssociation.vue +0 -332
- package/src/components/AppCompMediaPlayer.vue +0 -397
- package/src/plugins/timeManager.js +0 -77
- package/src/routes_bckp.js +0 -313
- package/src/routes_static.js +0 -344
- 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:
|
|
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
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</
|
|
18
|
-
</
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
256
|
+
mounted() {
|
|
257
|
+
// set the language of the app
|
|
258
|
+
this.setLocale(this.getAppConfigs.lang)
|
|
168
259
|
},
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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 =
|
|
354
|
-
this.
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
537
|
-
//width: $widthCtrSubtitle;
|
|
538
|
-
width: 10%;
|
|
539
|
-
}
|
|
814
|
+
this.sendXapiStatements([stmtPlaybar, stmt, endStmt], null, option) //send xapi statement
|
|
540
815
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
top:
|
|
575
|
-
left:
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
color: #fff !important;
|
|
1019
|
+
.grp-spinners span {
|
|
1020
|
+
margin: 0px 2px;
|
|
580
1021
|
}
|
|
1022
|
+
}
|
|
581
1023
|
|
|
582
|
-
|
|
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
|
-
.
|
|
612
|
-
|
|
1043
|
+
.box {
|
|
1044
|
+
width: 100%;
|
|
1045
|
+
height: 100%;
|
|
1046
|
+
position: relative;
|
|
1047
|
+
}
|
|
613
1048
|
}
|
|
614
1049
|
</style>
|