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