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