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