fcad-core-dragon 2.0.0-beta.0 → 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.
Files changed (161) hide show
  1. package/.editorconfig +6 -31
  2. package/.prettierrc +11 -0
  3. package/.vscode/extensions.json +8 -0
  4. package/.vscode/settings.json +16 -0
  5. package/CHANGELOG +153 -0
  6. package/README.md +28 -43
  7. package/documentation/.vitepress/config.js +114 -0
  8. package/documentation/api-examples.md +49 -0
  9. package/documentation/composants/app-base-button.md +58 -0
  10. package/documentation/composants/app-base-error-display.md +59 -0
  11. package/documentation/composants/app-base-popover.md +68 -0
  12. package/documentation/composants/app-comp-audio.md +75 -0
  13. package/documentation/composants/app-comp-branch-buttons.md +111 -0
  14. package/documentation/composants/app-comp-button-progress.md +53 -0
  15. package/documentation/composants/app-comp-carousel.md +53 -0
  16. package/documentation/composants/app-comp-container.md +53 -0
  17. package/documentation/composants/app-comp-input-checkbox-next.md +42 -0
  18. package/documentation/composants/app-comp-input-dropdown-next.md +34 -0
  19. package/documentation/composants/app-comp-input-radio-next.md +39 -0
  20. package/documentation/composants/app-comp-input-text-next.md +35 -0
  21. package/documentation/composants/app-comp-input-text-table-next.md +34 -0
  22. package/documentation/composants/app-comp-input-text-to-fill-dropdown-next.md +53 -0
  23. package/documentation/composants/app-comp-input-text-to-fill-next.md +31 -0
  24. package/documentation/composants/app-comp-jauge.md +31 -0
  25. package/documentation/composants/app-comp-menu-item.md +55 -0
  26. package/documentation/composants/app-comp-menu.md +29 -0
  27. package/documentation/composants/app-comp-navigation.md +41 -0
  28. package/documentation/composants/app-comp-note-call.md +53 -0
  29. package/documentation/composants/app-comp-note-credit.md +53 -0
  30. package/documentation/composants/app-comp-play-bar-next.md +53 -0
  31. package/documentation/composants/app-comp-pop-up-next.md +93 -0
  32. package/documentation/composants/app-comp-quiz-next.md +235 -0
  33. package/documentation/composants/app-comp-quiz-recall.md +53 -0
  34. package/documentation/composants/app-comp-svg-next.md +53 -0
  35. package/documentation/composants/app-comp-table-of-content.md +50 -0
  36. package/documentation/composants/app-comp-video-player.md +82 -0
  37. package/documentation/composants.md +46 -0
  38. package/documentation/composants_critiques/ModelPageComposant.md +53 -0
  39. package/documentation/composants_critiques/app-base-module.md +43 -0
  40. package/documentation/composants_critiques/app-base-page.md +48 -0
  41. package/documentation/composants_critiques/app-base.md +311 -0
  42. package/documentation/composants_critiques/main.md +15 -0
  43. package/documentation/demarrage.md +50 -0
  44. package/documentation/deploiement.md +58 -0
  45. package/documentation/index.md +33 -0
  46. package/documentation/markdown-examples.md +85 -0
  47. package/documentation/public/npm_version.png +0 -0
  48. package/documentation/public/vite.svg +15 -0
  49. package/documentation/public/vuejs.svg +2 -0
  50. package/documentation/public/vuetify.svg +6 -0
  51. package/eslint.config.js +60 -0
  52. package/package.json +43 -49
  53. package/src/$locales/en.json +94 -92
  54. package/src/$locales/fr.json +77 -113
  55. package/src/assets/data/onboardingMessages.json +1 -1
  56. package/src/components/AppBase.vue +971 -437
  57. package/src/components/AppBaseButton.test.js +21 -0
  58. package/src/components/AppBaseButton.vue +42 -10
  59. package/src/components/AppBaseErrorDisplay.vue +209 -162
  60. package/src/components/AppBaseFlipCard.vue +1 -0
  61. package/src/components/AppBaseModule.vue +1021 -976
  62. package/src/components/AppBasePage.vue +656 -128
  63. package/src/components/AppBasePopover.vue +41 -0
  64. package/src/components/AppBaseSkeleton.vue +66 -0
  65. package/src/components/AppCompAudio.vue +256 -0
  66. package/src/components/AppCompBranchButtons.vue +108 -181
  67. package/src/components/AppCompButtonProgress.vue +44 -70
  68. package/src/components/AppCompCarousel.vue +284 -233
  69. package/src/components/AppCompContainer.vue +29 -0
  70. package/src/components/AppCompInputCheckBoxNx.vue +323 -0
  71. package/src/components/AppCompInputDropdownNx.vue +299 -0
  72. package/src/components/AppCompInputRadioNx.vue +284 -0
  73. package/src/components/AppCompInputTextNx.vue +153 -0
  74. package/src/components/AppCompInputTextTableNx.vue +202 -0
  75. package/src/components/AppCompInputTextToFillDropdownNx.vue +340 -0
  76. package/src/components/AppCompInputTextToFillNx.vue +313 -0
  77. package/src/components/AppCompJauge.vue +36 -11
  78. package/src/components/AppCompMenu.vue +312 -92
  79. package/src/components/AppCompMenuItem.vue +129 -105
  80. package/src/components/AppCompNavigation.vue +972 -0
  81. package/src/components/AppCompNoteCall.vue +161 -0
  82. package/src/components/AppCompNoteCredit.vue +491 -0
  83. package/src/components/AppCompPlayBarNext.vue +2288 -0
  84. package/src/components/AppCompPopUpNext.vue +504 -0
  85. package/src/components/AppCompQuizNext.vue +510 -0
  86. package/src/components/AppCompQuizRecall.vue +350 -0
  87. package/src/components/AppCompSVGNext.vue +346 -0
  88. package/src/components/AppCompSettingsMenu.vue +18 -16
  89. package/src/components/AppCompTableOfContent.vue +340 -122
  90. package/src/components/AppCompVideoPlayer.vue +377 -0
  91. package/src/components/BaseModule.vue +24 -117
  92. package/src/components/tests__/AppBaseButton.spec.js +53 -0
  93. package/src/components/tests__/useTimer.spec.js +91 -0
  94. package/src/composables/useIdleDetector.js +56 -0
  95. package/src/composables/useQuiz.js +89 -0
  96. package/src/composables/useTimer.js +172 -0
  97. package/src/directives/nvdaFix.js +53 -0
  98. package/src/externalComps/ModuleView.vue +22 -0
  99. package/src/externalComps/SummaryView.vue +91 -0
  100. package/src/main.js +400 -142
  101. package/src/module/stores/appStore.js +947 -0
  102. package/src/module/xapi/ADL.js +241 -60
  103. package/src/module/xapi/Crypto/Hasher.js +8 -8
  104. package/src/module/xapi/Crypto/WordArray.js +6 -6
  105. package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +4 -4
  106. package/src/module/xapi/Crypto/algorithms/C_algo.js +14 -18
  107. package/src/module/xapi/Crypto/algorithms/HMAC.js +1 -1
  108. package/src/module/xapi/Crypto/algorithms/SHA1.js +1 -1
  109. package/src/module/xapi/Crypto/encoders/Base.js +7 -7
  110. package/src/module/xapi/Crypto/encoders/Base64.js +3 -3
  111. package/src/module/xapi/Crypto/encoders/Hex.js +4 -3
  112. package/src/module/xapi/Crypto/encoders/Latin1.js +3 -3
  113. package/src/module/xapi/Crypto/encoders/Utf8.js +3 -3
  114. package/src/module/xapi/Statement/index.js +3 -3
  115. package/src/module/xapi/launch.js +10 -10
  116. package/src/module/xapi/utils.js +17 -17
  117. package/src/module/xapi/wrapper.js +223 -218
  118. package/src/module/xapi/xapiStatement.js +29 -29
  119. package/src/plugins/analytics.js +34 -0
  120. package/src/plugins/bus.js +7 -2
  121. package/src/plugins/gsap.js +9 -8
  122. package/src/plugins/helper.js +141 -43
  123. package/src/plugins/i18n.js +13 -18
  124. package/src/plugins/idb.js +46 -30
  125. package/src/plugins/save.js +1 -1
  126. package/src/plugins/scorm.js +15 -15
  127. package/src/plugins/xapi.js +2 -2
  128. package/src/public/index.html +23 -11
  129. package/src/router/index.js +57 -0
  130. package/src/router/routes.js +312 -0
  131. package/src/shared/generalfuncs.js +241 -10
  132. package/src/shared/validators.js +1018 -0
  133. package/vitest.config.js +19 -0
  134. package/.eslintignore +0 -29
  135. package/.eslintrc.js +0 -86
  136. package/.prettierrc.js +0 -5
  137. package/babel.config.js +0 -3
  138. package/src/components/AppBaseDragChoice.vue +0 -91
  139. package/src/components/AppBaseDropZone.vue +0 -112
  140. package/src/components/AppCompDragAndDrop.vue +0 -339
  141. package/src/components/AppCompInputAssociation.vue +0 -332
  142. package/src/components/AppCompInputCheckBox.vue +0 -227
  143. package/src/components/AppCompInputDropdown.vue +0 -184
  144. package/src/components/AppCompInputRadio.vue +0 -169
  145. package/src/components/AppCompInputTextBox.vue +0 -91
  146. package/src/components/AppCompInputTextTable.vue +0 -155
  147. package/src/components/AppCompInputTextToFillDropdown.vue +0 -255
  148. package/src/components/AppCompInputTextToFillText.vue +0 -164
  149. package/src/components/AppCompMediaPlayer.vue +0 -365
  150. package/src/components/AppCompNavigationFull.vue +0 -1791
  151. package/src/components/AppCompPlayBar.vue +0 -1540
  152. package/src/components/AppCompPopUp.vue +0 -523
  153. package/src/components/AppCompQuiz.vue +0 -2998
  154. package/src/components/AppCompToolTip.vue +0 -94
  155. package/src/mixins/$pageMixins.js +0 -381
  156. package/src/mixins/$quizMixins.js +0 -456
  157. package/src/mixins/timerMixin.js +0 -132
  158. package/src/module/store.js +0 -874
  159. package/src/plugins/timeManager.js +0 -77
  160. package/src/routes.js +0 -734
  161. package/vue.config.js +0 -83
@@ -1,127 +1,124 @@
1
1
  <!--
2
2
  *@ Description: This component is used as main container to display application and containt to display
3
3
  *@ What it does: The component fetch the data for the page to display on navigation.
4
- * controle and scorm & xAPI logic and communication
4
+ *
5
5
  *@Note :Must be used
6
6
  -->
7
7
 
8
8
  <template>
9
9
  <div fluid class="module">
10
+ <span
11
+ id="page_info_section"
12
+ class="sr-only"
13
+ aria-labelledby="page_info"
14
+ ></span>
10
15
  <a class="skip-link" href="" @click.prevent="skipToMain">
11
16
  {{ $t('message.skip_content') }}
12
17
  </a>
13
- <portal to="playbar-portal">
14
- <app-comp-play-bar :app-status="isReady" :media-to-show="mediaToShw" />
15
- </portal>
16
- <b-navbar :key="$route.fullpath" class="app-nav">
17
- <!------------------------ Nav for drMode ------------------>
18
- <!-- <app-comp-table-of-content /> -->
19
- <!------------------------ Nav for FullMode ---------------->
20
- <app-comp-navigation-full
18
+
19
+ <nav
20
+ v-show="!isMenu"
21
+ id="navTool"
22
+ :key="$route.fullpath"
23
+ class="app-nav"
24
+ :class="{ show: closeDelay }"
25
+ >
26
+ <app-comp-navigation
27
+ :app-status="appReady ? true : null"
21
28
  :auto-navigate="theNavigationBetweenActivity"
22
- :app-status="isReady"
23
29
  />
24
- </b-navbar>
25
-
30
+ </nav>
26
31
  <base-module :m-data="$data">
27
- <b-container
32
+
33
+
34
+ <v-container
28
35
  id="wrapper-content"
29
36
  fluid
30
- :style="wrapperHeigth"
31
37
  :class="{ active: moduleConfig.videoFull }"
32
38
  class="scroll-bar"
33
39
  >
34
- <div
35
- v-show="!appReady"
36
- id="Loading"
37
- key="Loading"
38
- :class="{ fadeIn: !appReady && changePage }"
39
- >
40
- <!-- App ready: {{ appReady }} -->
41
- <div class="overlay" />
42
- <div class="box-loading-anime disableBlur">
43
- <img src="../assets/img/BeanEater-1s-200px.svg" alt="loading" />
44
- </div>
45
- </div>
46
40
  <div class="box">
47
- <router-view v-show="appReady" :key="$route.fullpath" ref="main" />
41
+ <router-view ref="main" :key="$route.fullPath" />
42
+ <!-- <router-view v-show="appReady" ref="main" :key="$route.fullPath" /> -->
48
43
  </div>
49
- <portal-target
50
- id="primary_nav_wrapper"
51
- name="primary_nav"
52
- ></portal-target>
53
- </b-container>
44
+
45
+ <div id="primary_nav_wrapper"></div>
46
+ </v-container>
54
47
  </base-module>
55
48
 
56
49
  <!------------------POPUP QUICK -------------------------->
57
- <app-comp-pop-up v-slot:content />
50
+ <app-comp-pop-up-next v-show="popupIsOpen">
51
+ <!-- <template #content></template> -->
52
+ </app-comp-pop-up-next>
58
53
  <!------------------END USE POP UP-------------------------->
59
- <!------------------tooltip -------------------------->
60
- <app-comp-tool-tip
61
- v-if="toolTipTarget !== ''"
62
- v-slot:content
63
- :target="toolTipTarget"
64
- />
65
- <!------------------END USE tooltip-------------------------->
66
- <!------------------Settings Splash-screeen----------------------------->
67
- <b-modal
68
- id="splashscreen"
69
- ref="splashscreen"
70
- :title="$t('label.splashscreen')"
71
- size="lg"
72
- ok-only
73
- no-stacking
74
- :ok-title="$t('user_settings.close')"
75
- :header-close-label="$t('button.closePopUp')"
76
- @hidden="onSplashClose"
77
- >
78
- <span>
79
- {{ $t('message.splash_welcome') }}
80
- </span>
81
- <b-form-group
82
- id="splash-form"
83
- v-slot="{ ariaDescribedby }"
84
- :label="$t('user_settings.title')"
54
+
55
+ <!--------------RIGHT SIDEBAR (for display of extra contents)------------>
56
+ <Transition name="right-sidebar-transition" mode="out-in">
57
+ <section
58
+ v-if="rightSidebarVisible"
59
+ id="right-sidebar"
60
+ ref="right-sidebar"
61
+ :key="dynamicSidebarContent._id"
62
+ :aria-label="$t('label.side_panel')"
63
+ :class="{
64
+ 'v-media': dynamicSidebarContent._context === 'ctxTranscript'
65
+ }"
85
66
  >
86
- <b-form-checkbox
87
- v-for="(setting, index) in Object.entries(settingsSelected)"
88
- :key="index"
89
- v-model="settingsSelected[setting[0]]"
90
- :aria-describedby="ariaDescribedby"
91
- switch
92
- stacked
93
- size="lg"
94
- :name="`parameter-${setting[0]}`"
95
- :text-field="$t(`user_settings.${setting[0]}`)"
96
- >
97
- <span>{{ $t(`user_settings.${setting[0]}`) }}</span>
98
- </b-form-checkbox>
99
- </b-form-group>
100
- </b-modal>
101
- <!------------------END Settings splash-screen-------------------------->
67
+ <div id="right-sidebar-header">
68
+ <app-base-button
69
+ :title="$t('button.closePopUp')"
70
+ :aria-label="$t('button.closePopUp')"
71
+ class="btn-reserve-ico embranchement-close"
72
+ @click="
73
+ closeSidebar(
74
+ dynamicSidebarContent._context,
75
+ dynamicSidebarContent._container
76
+ ? dynamicSidebarContent._container
77
+ : null
78
+ )
79
+ "
80
+ >
81
+ <svg aria-hidden="true" focusable="false">
82
+ <use href="#close-square-icon" />
83
+ </svg>
84
+ </app-base-button>
85
+ </div>
86
+ <div id="right-sidebar-body">
87
+ <component
88
+ :is="dynamicSidebarContent._component"
89
+ class="v-media"
90
+ v-bind="{ ...dynamicSidebarContent._comProps }"
91
+ />
92
+ <!-- </Transition> -->
93
+ </div>
94
+ <div id="right-sidebar-footer"></div>
95
+ </section>
96
+ </Transition>
97
+ <footer></footer>
98
+ <!-------------------------END RIGHT SIDEBAR------------------------------->
102
99
  </div>
103
100
  </template>
104
101
  <script>
105
- import { mapGetters } from 'vuex'
102
+ // const modules = import.meta.glob('@/module/**/*.vue')
103
+ import { fileAssets } from '../shared/generalfuncs.js'
104
+ import { mapState, mapActions } from 'pinia'
105
+ import { useAppStore } from '../module/stores/appStore'
106
106
  import BaseModule from './BaseModule.vue'
107
- import { timerMixin } from '../mixins/timerMixin'
107
+ import AppCompContainer from './AppCompContainer.vue'
108
+ import { defineAsyncComponent } from 'vue'
108
109
  //import
109
110
  export default {
110
111
  components: {
111
112
  BaseModule,
113
+ AppCompContainer
112
114
  },
113
- mixins: [timerMixin],
115
+ inject: ['elapsedIdleTime', 'lessonDuration', 'appTimer'],
114
116
  props: {
115
117
  moduleConfig: {
116
118
  type: Object,
117
119
  default: () => {
118
120
  return {
119
- id: 'mod_001',
120
- title: null, // edit to set the title of the lesson
121
- description: null,
122
- introActive: false, // Controle use INTODUCTION page in the Lesson. set to false if there is no introduction
123
- concluActive: false, // Controle use CONCLUSION page in the Lesson. set to false if there is no conclusion
124
- allowNavigationToActivity: false, // set Previous/Next can allow navigation between activities. Set to false if do not want navigation between activities with Previous/Next,É
121
+ allowNavigationToActivity: false, // set Previous/Next can allow navigation between activities. Set to false if do not want navigation between activities with Previous/Next
125
122
  main: ''
126
123
  }
127
124
  }
@@ -131,63 +128,81 @@ export default {
131
128
  return {
132
129
  meta: {},
133
130
  videoFull: null,
134
- routeData: null,
131
+ routeData: [],
135
132
  changePage: false,
136
- appReady: false,
137
- wrapperHeigth: null,
138
- mediaToShw: {},
139
133
  randKey: Math.floor(Math.random() * 10001),
140
- popupIsOpen: null,
134
+ popupIsOpen: false,
141
135
  hidePlayBar: false, // Controle visibility of the play bar. set to true to hide play bar
142
136
  stmt: null, // holder for xapi statememt,
143
- moduleTimer: 0, //tracker for overall time spent the lesson
144
- activityTimer: 0, // tracker for the time spent on activity,
145
137
  routeChangeCounter: 0,
146
138
  toolTipTarget: '', //for the tool tip,
147
139
  onboardingMessages: {}, //for the onboarding @todo replace with default file
148
- settingsSelected: {}
140
+ settingsSelected: {},
141
+ compID: null,
142
+ rightSidebarVisible: false,
143
+ lastInFocus: null,
144
+ closeDelay: false,
145
+ timeOut: null,
146
+ infocusTabIndex: null,
147
+ transcriptVisible: false,
148
+ transcriptContent: null,
149
+ transcriptContainer: null,
150
+ branchingVisible: false,
151
+ customContentVisible: false,
152
+ customContent: null,
153
+ lessonCompletionStatus: false,
154
+ rightSidebarEvent: new CustomEvent('sidebarEvent', {
155
+ bubbles: true,
156
+ detail: {
157
+ text: () => 'this is a test',
158
+ name: 'sidebarEvent'
159
+ }
160
+ }),
161
+ lessonStarted: false
149
162
  }
150
163
  },
151
164
  computed: {
152
- ...mapGetters([
165
+ ...mapState(useAppStore, [
153
166
  'getCurrentPage',
167
+ 'getCurrentBranchPage',
154
168
  'getAppStatus',
155
169
  'getUserInteraction',
156
170
  'getModuleInfo',
157
- 'getCurrentBrowser',
158
171
  'getAllActivities',
159
172
  'getAllCompleted',
160
173
  'getConnectionInfo',
161
174
  'hasMediaElOrTimeline',
162
- 'getModuleChildren',
163
- 'getAllActivities',
164
- 'getPageData',
165
175
  'getMenuSettings',
166
- 'getCurrentSection',
167
176
  'getRouteHistory',
168
177
  'getOnboardingEnabled',
169
- 'getApplicationSettings'
178
+ 'getApplicationSettings',
179
+ 'getCompStatusTracker',
180
+ 'getLessonPosition',
181
+ 'getCompletionState'
170
182
  ]),
171
-
172
- isReady() {
173
- let readyState
174
- if (this.getAppStatus === 'loading') readyState = false
175
- else if (this.getAppStatus === 'ready') readyState = true
176
- return readyState
183
+ isMenu() {
184
+ return this.$route.name === 'menu'
185
+ },
186
+ appReady() {
187
+ return this.getAppStatus === 'ready' ? true : false
177
188
  },
178
-
179
189
  hasMedia() {
180
190
  return typeof this.hasMediaElOrTimeline === 'object'
181
191
  },
192
+ activityHasChanged() {
193
+ const rd = this.routeData.toReversed()
182
194
 
183
- displayLang() {
184
- let lang = false
185
- const displayList = ['en-US', 'fr-FR', 'es-ES'] // list of xapi verbs language display
195
+ const routeWasMenu =
196
+ this.$router.options.history.state.back &&
197
+ this.$router.options.history.state.back.includes('menu')
186
198
 
187
- if (this.getModuleInfo.packageType === 'xapi') {
188
- lang = displayList.find((l) => l.includes(this.$i18n.locale))
189
- }
190
- return lang
199
+ if (
200
+ rd.length <= 1 ||
201
+ rd[0].activity_ref !== this.$route.meta.activity_ref ||
202
+ routeWasMenu
203
+ )
204
+ return true
205
+ else return false
191
206
  },
192
207
  /**
193
208
  * @description Set the id the module
@@ -203,7 +218,8 @@ export default {
203
218
  theTitle() {
204
219
  let title = this.$t(`text.place_holder.for_lesson_title`)
205
220
 
206
- if (this.moduleConfig.title) title = this.moduleConfig.title
221
+ if (this.getMenuSettings.lessonTitle)
222
+ title = this.getMenuSettings.lessonTitle
207
223
  return title
208
224
  },
209
225
  /**
@@ -217,24 +233,12 @@ export default {
217
233
  description = this.moduleConfig.description
218
234
  return description
219
235
  },
220
- /**
221
- * @description Set default value for bookmark
222
- */
223
- bookmarkActive() {
224
- let bookmarkState
225
-
226
- if (this.moduleConfig.bookmarkActive)
227
- bookmarkState = this.moduleConfig.bookmarkActive
228
- else bookmarkState = false
229
- return bookmarkState
230
- },
231
236
  /**
232
237
  * @description set Previous/Next can allow navigation between activities.
233
238
  *
234
239
  */
235
240
  theNavigationBetweenActivity() {
236
241
  let navBwteenActivity = false
237
-
238
242
  if (this.moduleConfig.allowNavigationToActivity)
239
243
  navBwteenActivity = this.moduleConfig.allowNavigationToActivity
240
244
  return navBwteenActivity
@@ -245,7 +249,6 @@ export default {
245
249
  */
246
250
  theIntroIsActivated() {
247
251
  let introActive = false
248
-
249
252
  if (this.moduleConfig.introActive)
250
253
  introActive = this.moduleConfig.introActive
251
254
  return introActive
@@ -259,108 +262,186 @@ export default {
259
262
  if (!mainEl) return this.$route.meta.id
260
263
 
261
264
  return mainEl
265
+ },
266
+ dynamicSidebarContent() {
267
+ if (
268
+ !this.transcriptVisible &&
269
+ !this.branchingVisible &&
270
+ !this.customContentVisible
271
+ )
272
+ return null
273
+ let sidebarSettings = {}
274
+ let _label = null
275
+ //=========================================
276
+ switch (true) {
277
+ case this.transcriptVisible:
278
+ _label =
279
+ this.$i18n.locale === 'fr'
280
+ ? 'Contenu de la transcription'
281
+ : 'Content of the transcript'
282
+
283
+ sidebarSettings = {
284
+ _component: AppCompContainer,
285
+ _comProps: {
286
+ content: this.transcriptContent,
287
+ id: 'transcript-content'
288
+ },
289
+ _context: 'ctxTranscript',
290
+ _container: this.transcriptContainer,
291
+ _label,
292
+ _id: 'transcript'
293
+ }
294
+ break
295
+
296
+ case this.branchingVisible: {
297
+ const allActivities = fileAssets.getActivities()
298
+ if (this.$route.meta.type !== 'branching' || !this.compID) return null
299
+
300
+ const componentName = this.compID
301
+ const { activityRef } = this.getCurrentPage //get activity id from current page
302
+
303
+ _label =
304
+ this.$i18n.locale === 'fr'
305
+ ? "contenu de l'embranchement"
306
+ : 'content of the branching'
307
+
308
+ sidebarSettings = {
309
+ _component: defineAsyncComponent(async () => {
310
+ const compFile = allActivities.filter((f) => {
311
+ return f.name.includes(`/${activityRef}/${componentName}`)
312
+ })[0]
313
+
314
+ return compFile.content
315
+ }),
316
+
317
+ _comProps: false,
318
+ _context: 'ctxBranching',
319
+ _label,
320
+ _id: componentName
321
+ }
322
+ break
323
+ }
324
+
325
+ case this.customContentVisible:
326
+ sidebarSettings = {
327
+ _component: AppCompContainer,
328
+ _comProps: {
329
+ content: this.customContent
330
+ },
331
+ _context: 'ctxCustomContent',
332
+ _label,
333
+ _id: 'custom-content'
334
+ }
335
+ break
336
+ }
337
+
338
+ return sidebarSettings
339
+ },
340
+ navigationHistory() {
341
+ return this.getRouteHistory
262
342
  }
263
343
  },
264
344
  watch: {
265
- $route: {
345
+ navigationHistory: {
266
346
  handler() {
267
- if (this.getRouteHistory) {
268
- this.routeData = this.getRouteHistory
269
- } else {
270
- this.routeData = []
271
- }
272
-
273
- this.$router.beforeResolve((to, from, next) => {
274
- this.updateContent(to, from, next)
275
- if (!this.routeData.length) {
276
- this.routeData.push(from.meta)
277
- this.$store.dispatch('setRouteHistory', this.routeData)
278
- }
347
+ this.routeData = this.navigationHistory
348
+ },
349
+ deep: true,
350
+ immediate: true
351
+ },
352
+ $route: {
353
+ handler(newValue) {
354
+ this.getFocusables().then((r) => {
355
+ //Pressing Tab or Shit + tab should make it possible to cycle focus through them
356
+ if (!r) return
357
+ r[0].focus()
358
+ this.infocusTabIndex = r[0]
279
359
  })
280
360
 
281
361
  //update the routeChangeCounter when navigation
282
362
  if (this.routeChangeCounter < 3) this.routeChangeCounter += 1
283
363
 
284
- this.routeData.push(this.$route.meta)
364
+ /*
365
+ *Start Timer on New activities
366
+ */
285
367
 
286
- if (this.routeData.length >= 4) {
287
- this.routeData.shift()
368
+ const trackedRouteType = [
369
+ 'introduction',
370
+ 'conclusion',
371
+ 'normal',
372
+ 'branching'
373
+ ]
374
+ if (trackedRouteType.includes(this.$route.meta.type)) {
375
+ //Navigation should only trigger timer and xapi statement when app is ready and activity has changed
376
+ if (!this.activityHasChanged || !this.appReady) return
377
+
378
+ //Start the timer every time route is a new activity
379
+ this.$bus.$emit('start-timer')
380
+ //Send statement when activity as changed
381
+ this.sendStartStatement({ id: this.$route.meta.activity_ref })
288
382
  }
383
+ this.transcriptVisible = false
384
+ },
385
+ deep: true,
386
+ immediate: true
387
+ },
289
388
 
290
- this.$store.dispatch('setRouteHistory', this.routeData)
389
+ appReady: {
390
+ async handler(newValue) {
391
+ //Should only start timer when the app has fully loaded
291
392
 
292
- /* send xapi statement only when there is a change of activity:
293
- Activity has changed when the type of the route is one of the following or the activity__ref or previous route is different of activity_ref of current route
294
- */
295
- const trackedRouteType = ['menu', 'introduction', 'activity']
393
+ if (!this.lessonStarted) this.initLesson()
296
394
 
297
- if (
298
- trackedRouteType.includes(this.$route.meta.type) &&
299
- this.getModuleInfo.packageType === 'xapi' &&
300
- this.getConnectionInfo &&
301
- this.getConnectionInfo.actor &&
302
- this.getConnectionInfo.remote
303
- ) {
304
- const stmt = {
305
- id: `${this.$route.name.split('.')[0]}`,
306
- description: `${this.$route.name.split('.')[0]} de ${
307
- this.getModuleInfo.id
308
- }`,
309
- definition: `${this.$route.name.split('.')[0]} de ${
310
- this.getModuleInfo.id
311
- }`
312
- }
313
-
314
- //Start the timer every time that there is a new activity
315
- if (
316
- this.$route.meta.type === 'activity' ||
317
- this.$route.meta.type === 'introduction'
318
- ) {
319
- if (this.timerState === 'started') this.stopTimer('activity') // reset the activity timer
320
- this.startTimer('activity')
321
- }
322
-
323
- this.sendXapiStatement(stmt)
395
+ if (this.activityHasChanged && newValue) {
396
+ this.$bus.$emit('start-timer')
324
397
  }
325
-
326
-
327
398
  },
328
399
  immediate: true
329
400
  },
401
+
330
402
  /**
331
- * @description Defined in Store
403
+ * @description Defined in Store Watch all completed activities to send a lesson completion statement
332
404
  */
333
- getAppStatus(newValue) {
334
- if (newValue === 'ready') {
335
- this.appReady = true
336
- } else {
337
- this.appReady = false
338
- }
405
+ getAllCompleted: {
406
+ handler() {
407
+ // Check once the server to set the completion status of the lesson
408
+ if (this.getCompletionState) {
409
+ const completedState = this.getCompletionState
410
+
411
+ this.lessonCompletionStatus =
412
+ !completedState || !completedState.completion
413
+ ? false
414
+ : completedState.completion
415
+ }
416
+
417
+ if (!this.lessonCompletionStatus) this.sendCompletionStatus('LESSON')
418
+ },
419
+ deep: true,
420
+ immediate: true
339
421
  },
340
422
  /**
341
- * @description Defined in Store Watch all completed activities to send a lesson completion statement
423
+ * @description Defined in timerMixin Watch The epalsed time for autosaving the user reached position
342
424
  */
343
- getAllCompleted: {
425
+ elapsedIdleTime: {
344
426
  handler() {
427
+ if (this.appTimer.getTimerState() !== 'started') return
428
+
429
+ //send a statement every x time (second)
345
430
  if (
431
+ this.elapsedIdleTime % 500 === 0 &&
346
432
  this.getModuleInfo.packageType === 'xapi' &&
347
433
  this.getConnectionInfo &&
348
434
  this.getConnectionInfo.actor &&
349
435
  this.getConnectionInfo.remote
350
436
  ) {
351
- //======================================================
352
- let completedSize = 0
353
- Object.entries(this.getAllCompleted).forEach((value) => {
354
- completedSize = completedSize + value[1].length
355
- })
356
- const completedState = this.$xapi._getLessonStatus(
357
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
358
- this.getConnectionInfo.activity_id
359
- )
437
+ const lessonPosition = this.getLessonPosition
438
+
439
+ const lastReached = lessonPosition.length ? lessonPosition[0] : ''
360
440
 
441
+ //only Send savepoint statement when 3 navigation happen and the route is different from last saved
361
442
  if (
362
- completedSize === this.getAllActivities.pageSize &&
363
- !completedState.completion
443
+ this.routeChangeCounter >= 3 &&
444
+ lastReached !== this.$route.name
364
445
  ) {
365
446
  let text
366
447
  //Defining the text to display for stmt description and definition
@@ -377,101 +458,68 @@ export default {
377
458
  break
378
459
  }
379
460
 
380
- // Retrive only user data in lessons front its interaction that we want to send to the LRS.
381
- // Note: User Settings are sent on a different URI and statement
382
- const {
383
- isFistTime,
384
- userSettings,
385
- ...lessonsData
386
- } = this.getUserInteraction
387
-
388
- this.sendXapiStatement({
389
- verb: 'completed',
461
+ //Creating custom statment
462
+ const stmt = {
463
+ verb: 'progressed',
390
464
  definition: text,
391
465
  description: text,
392
- extension: [
466
+ extensions: [
393
467
  {
394
- id: 'user-data',
395
- content: {
396
- routeHistory: this.getRouteHistory,
397
- ...lessonsData
398
- }
468
+ id: 'ending-point',
469
+ content: (() =>
470
+ this.$route.name == 'menu'
471
+ ? this.$helper.getRoutesFromVueRouter().meta.children[0]
472
+ ._namedRoute
473
+ : this.$route.name)()
399
474
  }
400
475
  ],
401
- duration: this.lessonDuration,
402
- completion: true
403
- })
404
- }
405
- }
406
-
407
- //================================================================
408
- },
409
- immediate: true
410
- },
411
- /**
412
- * @description Defined in timerMixin Watch The epalsed time for autosaving the user reached position
413
- */
414
- elapsedTime() {
415
- if (this.timerState !== 'started') return
416
- //send a statement every x time (second)
417
- if (
418
- (this.timerState === 'started',
419
- this.elapsedTime % 30 === 0 &&
420
- this.getModuleInfo.packageType === 'xapi' &&
421
- this.getConnectionInfo &&
422
- this.getConnectionInfo.actor &&
423
- this.getConnectionInfo.remote)
424
- ) {
425
- const lastReached = this.$xapi._getLessonPosition(
426
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
427
- this.getConnectionInfo.activity_id
428
- )[0]
429
-
430
- //only Send savepoint statement when 3 navigation happen and the route is different from last saved
431
- if (this.routeChangeCounter >= 3 && lastReached !== this.$route.name) {
432
- let text
433
- //Defining the text to display for stmt description and definition
434
- switch (this.$i18n.locale) {
435
- case 'fr':
436
- if (this.getModuleInfo.courseID)
437
- text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
438
- else text = `Le ${this.getModuleInfo.id}`
439
- break
440
- case 'en':
441
- if (this.getModuleInfo.courseID)
442
- text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
443
- else text = `The ${this.getModuleInfo.id}`
444
- break
445
- }
476
+ duration: this.appTimer.ISOTimeParser(this.lessonDuration)
477
+ }
446
478
 
447
- //Creating custom statment
448
- const stmt = {
449
- verb: 'progressed',
450
- definition: text,
451
- description: text,
452
- extension: [
453
- {
454
- id: 'ending-point',
455
- content: this.$route.name
456
- }
457
- ],
458
- duration: this.lessonDuration
479
+ this.$bus.$emit('send-xapi-statement', stmt)
480
+ this.routeChangeCounter = 0 //reset counter after saving
459
481
  }
460
-
461
- this.sendXapiStatement(stmt)
462
- this.routeChangeCounter = 0 //reset counter after saving
463
482
  }
464
- }
483
+ },
484
+ immediate: true,
485
+ deep: true
465
486
  }
466
487
  },
467
-
488
+ beforeUnmount() {
489
+ //Communication events
490
+ this.$bus.$off('close-sidebar', this.closeSidebar)
491
+ this.$bus.$off('open-sidebar', this.openSidebar)
492
+ this.$bus.$off('open-popup', this.openPopup)
493
+ this.$bus.$off('close-popup', this.closePopup)
494
+ this.$bus.$off('start-onboarding', this.startOnboarding)
495
+ this.$bus.$off('videoFullScreen', this.onVideoFullScreen)
496
+ this.$bus.$off('save-to-scorm', this.saveToScorm)
497
+ this.$bus.$off('launch-xapi-resource', this.launchResource)
498
+ this.$bus.$off('update-route-history', this.updateRouteHistory)
499
+ this.$bus.$off('update-content', this.updateContent)
500
+ this.$bus.$off('show-transcript', this.openTranscript)
501
+ this.$bus.$off('send-completion-event', this.sendCompletionStatus)
502
+ this.$bus.$off('send-starting-event', this.sendStartStatement)
503
+
504
+ //nav mouseleave event
505
+ let nav = document.getElementById('navTool')
506
+ if (nav) {
507
+ nav.removeEventListener('mouseleave', this.onNavMouseleave)
508
+ }
509
+ //sidebar scroll event
510
+ const rightSidebar = this.getRightSidebar()
511
+ if (rightSidebar) {
512
+ rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
513
+ }
514
+ //keayboard listener
515
+ document.removeEventListener('keydown', this.handleKeyboardControls)
516
+ },
468
517
  created() {
469
- //TODO: get course name from app settings
470
518
  let lessonLabel, lessonNumber, lessonTitle, titleString
471
519
  lessonLabel = this.$t('text.lesson')
472
520
  lessonNumber = this.moduleConfig.id.replace('module_', '')
473
521
  lessonTitle = this.theTitle
474
- titleString = lessonLabel + ' ' + lessonNumber + ' : ' + lessonTitle
522
+ titleString = lessonLabel + ' ' + lessonNumber + ' ' + lessonTitle
475
523
 
476
524
  //Remove prefix for introduction or conclusion, according to isIntroConclu setting in Module.vue
477
525
  if (typeof this.moduleConfig.isIntroConclu !== 'undefined') {
@@ -480,78 +528,186 @@ export default {
480
528
  }
481
529
  }
482
530
  document.title = titleString
483
-
484
- this.wrapperHeigth = `height:${window.innerHeight - 55}px`
531
+ document.addEventListener('sidebarEvent', (e) => {
532
+ // e. e.checkVisibility()
533
+ this.$bus.$emit(
534
+ 'side-bar-open',
535
+ e.target.checkVisibility({
536
+ opacityProperty: true,
537
+ visibilityProperty: true,
538
+ contentVisibilityAuto: true
539
+ })
540
+ )
541
+ })
485
542
  // Tell the store the state of intro and conclusion
486
- this.$store.commit('UPDATE_INTRO_STATE', this.theIntroIsActivated)
487
-
543
+ this.updateIntroStatus(this.theIntroIsActivated)
488
544
  //Communication events
489
- this.$bus.$on('open-popup', (data) => {
490
- this.openPopup(data)
491
- })
545
+ this.$bus.$on('open-popup', this.openPopup)
492
546
 
493
- this.$bus.$on('close-popup', () => {
494
- this.closePopup()
495
- })
547
+ this.$bus.$on('close-popup', this.closePopup)
496
548
 
497
- this.$bus.$on('start-onboarding', (data) => {
498
- this.startOnboarding(data)
499
- })
549
+ this.$bus.$on('start-onboarding', this.startOnboarding)
500
550
 
501
- this.$bus.$on('update-page', () => {
502
- this.mediaToShw = this.formateData(this.getCurrentPage)
503
- })
551
+ this.$bus.$on('open-sidebar', this.openSidebar)
504
552
 
505
- this.$bus.$on('videoFullScreen', (value) => {
506
- this.videoFull = value
507
- })
553
+ this.$bus.$on('close-sidebar', this.closeSidebar)
508
554
 
509
- this.$bus.$on('save-to-scorm', () => {
510
- this.saveToScorm()
511
- })
555
+ this.$bus.$on('videoFullScreen', this.onVideoFullScreen)
512
556
 
513
- this.$bus.$on(
514
- 'send-xapi-statement',
515
- (statement, cb = null, option = false) => {
516
- this.sendXapiStatement(statement, cb, option)
517
- }
518
- )
519
-
520
- this.$bus.$on('launch-xapi-resource', (res) => {
521
- this.launchResource(res)
522
- })
557
+ this.$bus.$on('save-to-scorm', this.saveToScorm)
523
558
 
524
- this.$bus.$on('fire-exit-event', this.endLesson)
559
+ this.$bus.$on('launch-xapi-resource', this.launchResource)
525
560
 
526
- this.$bus.$on('set-comp-status', (name, status) =>
527
- this.updateTracker(name, status)
528
- )
529
- this.initLesson()
561
+ this.$bus.$on('update-route-history', this.updateRouteHistory)
562
+ this.$bus.$on('update-content', this.updateContent)
563
+ this.$bus.$on('show-transcript', this.openTranscript)
564
+ this.$bus.$on('send-completion-event', this.sendCompletionStatus)
565
+ this.$bus.$on('send-starting-event', this.sendStartStatement)
530
566
 
531
- if (this.getRouteHistory.length != 0) {
532
- this.routeHistory = this.getRouteHistory
567
+ if (this.navigationHistory.length != 0) {
568
+ this.routeData = this.navigationHistory
533
569
  }
534
570
  setTimeout(() => {
535
571
  this.settingsSelected = { ...this.getApplicationSettings }
536
572
  }, 800)
537
573
  },
538
-
539
574
  mounted() {
540
- setTimeout(() => {
541
- this.updateTracker('AppBaseModule', 'loading')
542
- const { autoplay, onboarding } = this.getApplicationSettings
543
- let canShowSplash = false
544
-
545
- if (typeof autoplay !== 'undefined' && autoplay === null)
546
- canShowSplash = true
547
- if (typeof onboarding !== 'undefined' && onboarding === null)
548
- canShowSplash = true
549
- if (canShowSplash) this.showSplash()
550
-
551
- this.updateTracker('AppBaseModule', 'ready')
552
- }, 1500)
575
+ let nav = document.getElementById('navTool')
576
+
577
+ if (nav) nav.addEventListener('mouseleave', this.onNavMouseleave)
578
+
579
+ document.addEventListener('keydown', this.handleKeyboardControls)
553
580
  },
554
581
  methods: {
582
+ ...mapActions(useAppStore, [
583
+ 'updateIntroStatus',
584
+ 'updateCurrentTimeline',
585
+ 'updateCurrentMediaElements',
586
+ 'updateCurrentPage',
587
+ 'updatesideBIsOpen',
588
+ 'setCompletionState',
589
+ 'updatepopIsOpen'
590
+ ]),
591
+ onNavMouseleave() {
592
+ let widgetOpen = document.getElementsByClassName('open')
593
+
594
+ if (widgetOpen.length == 0) {
595
+ this.cancelTimeout()
596
+ this.closeDelay = true
597
+ this.startTimeout()
598
+ } else {
599
+ this.closeDelay = true
600
+ }
601
+ },
602
+ onVideoFullScreen(value) {
603
+ this.videoFull = value
604
+ },
605
+ /* Get All Element related to the media return a list of all DOM element*/
606
+ async getFocusables() {
607
+ return new Promise(function (resolve, reject) {
608
+ setTimeout(function () {
609
+ const getAllFocusables = () => {
610
+ const listItems = document.querySelectorAll('.v-media')
611
+ if (!listItems || !listItems.length) return null
612
+ return Array.from(listItems)
613
+ }
614
+ resolve(getAllFocusables())
615
+ }, 200)
616
+ })
617
+ },
618
+ /**
619
+ * @description Handle Keyboard controls
620
+ * @summary
621
+ * @param {Object} evt - event that fired the action
622
+ *
623
+ */
624
+ async handleKeyboardControls(evt) {
625
+ let { code } = evt
626
+
627
+ if (code === 'Escape' && this.rightSidebarVisible) {
628
+ this.closeSidebar(this.dynamicSidebarContent._context)
629
+ }
630
+ },
631
+
632
+ /**
633
+ * @description open the right sidebar component and show and set it content
634
+ * @summary opens sidebar according to context
635
+ * @param {Object} obj {ctx , e} - context and content to display in the sidebar
636
+ *
637
+ */
638
+ openSidebar(obj) {
639
+ if (!obj || !obj.ctx || !obj.e) return
640
+ const wrapper = obj.w ? obj.w : null
641
+ this.lastInFocus = document.activeElement
642
+ const { ctx, e: content, persist = false } = obj
643
+
644
+ switch (ctx) {
645
+ case 'ctxTranscript':
646
+ this.openTranscript(content, wrapper)
647
+ break
648
+
649
+ case 'ctxBranching':
650
+ if (this.compID === content && !persist)
651
+ return this.closeSidebar('ctxBranching')
652
+
653
+ this.openBranchContent(content)
654
+ break
655
+ case 'ctxCustomContent':
656
+ this.displayCustomContent(content, wrapper)
657
+ break
658
+ }
659
+ //delay animation
660
+ this.rightSidebarVisible = true
661
+ this.updatesideBIsOpen(this.rightSidebarVisible)
662
+ // setTimeout(() => {
663
+ const rightSidebarContent = this.getRightSidebar() // Emelent displayed in the sidebar-body
664
+ if (!rightSidebarContent) return
665
+ rightSidebarContent.scrollTop = 0
666
+ const rSidebar = document.querySelector('#right-sidebar') // the sidebar
667
+ rSidebar.dispatchEvent(this.rightSidebarEvent)
668
+ rSidebar.setAttribute('tabindex', -1)
669
+ this.resetFocus(rSidebar) //set focus on the sidebar
670
+ // }, 100)
671
+ },
672
+ /**
673
+ * @description close the right sidebar component
674
+ * @summary close sidebar according to context
675
+ * @param {String} ctx - context in which side bar was opened and will now be closed
676
+ *
677
+ */
678
+ closeSidebar(ctx, wrapper = null) {
679
+ //delay animation
680
+ this.rightSidebarVisible = false //
681
+ setTimeout(() => {
682
+ const rSidebar = document.querySelector('#right-sidebar') // the sidebar
683
+ if (!rSidebar) return
684
+ rSidebar.setAttribute('style', 'display:none')
685
+ rSidebar.dispatchEvent(this.rightSidebarEvent) //this will allow to run the animation of sidebar closing 1rst
686
+ }, 100)
687
+ this.resetFocus(this.lastInFocus)
688
+ switch (ctx) {
689
+ case 'ctxTranscript':
690
+ this.closeTranscript(wrapper)
691
+ break
692
+
693
+ case 'ctxBranching':
694
+ this.closeBranchContent()
695
+ break
696
+
697
+ case 'ctxCustomContent':
698
+ this.closeCostumContent()
699
+ break
700
+
701
+ default:
702
+ this.branchingVisible = false
703
+ this.transcriptVisible = false
704
+ this.customContentVisible = false
705
+ this.transcriptContent = null
706
+ this.customContent = null
707
+ this.compID = null
708
+ }
709
+ this.updatesideBIsOpen(this.rightSidebarVisible)
710
+ },
555
711
  /**
556
712
  * @description to close a pop up (not currently used)
557
713
  * @param {Function} [cb]
@@ -560,6 +716,7 @@ export default {
560
716
  closePopup(cb) {
561
717
  this.popupIsOpen = false
562
718
  this.$bus.$emit('popup-close', cb)
719
+ this.updatepopIsOpen(this.popupIsOpen) //update information in store
563
720
  },
564
721
  /**
565
722
  * @description to open a pop up
@@ -567,9 +724,9 @@ export default {
567
724
  * @fires popup-open to AppBasePopup.vue
568
725
  */
569
726
  openPopup(data) {
570
- this.popUpData = data // change the data in the popup
571
- this.$bus.$emit('popup-open', data) // Use to send message to popUp component
572
727
  this.popupIsOpen = true
728
+ this.$bus.$emit('popup-open', data) // Use to send message to popUp component
729
+ this.updatepopIsOpen(this.popupIsOpen) //update information in store
573
730
  },
574
731
 
575
732
  /**
@@ -589,60 +746,113 @@ export default {
589
746
  this.$bus.$emit('tooltip-open', data) // Use to send message to tooltip component
590
747
  },
591
748
 
592
- //===== splash screen methods ======//
593
749
  /**
594
- * @description to show settings Splash screen
750
+ * @description Manage opening of sidebar in transcript context
751
+ * @summary set the value of the transcriptContent and transcripVisibility
752
+ * @param {Object} c content to display in the sidebar
753
+ */
754
+
755
+ openTranscript(c, container) {
756
+ if (!c) return (this.transcriptVisible = false)
757
+ //Change transcript content
758
+ if (this.transcriptContent !== c) this.transcriptContent = c
759
+
760
+ this.transcriptContainer = container
761
+ this.transcriptVisible = true
762
+ },
763
+ /**
764
+ * @description Manage closing of sidebar in transcript context
765
+ * @summary reset the value of the transcriptContent and transcripVisibility
766
+ * @fires 'transcript-hidden' to AppCompPlaybar
595
767
  */
596
- showSplash() {
597
- this.$refs['splashscreen'].show()
598
- this.$set(this.getUserInteraction, 'isFirstTime', false)
768
+ closeTranscript(container = this.transcriptContainer) {
769
+ let t = this.customContentVisible ? 10 : 200
770
+ setTimeout(() => {
771
+ this.transcriptContent = null
772
+ this.transcriptVisible = false
773
+ }, t)
774
+ this.$bus.$emit('transcript-hidden')
775
+ this.$bus.$emit('resize-media', 'lg', container)
599
776
  },
600
777
 
601
778
  /**
602
- * @description to close settings splash screen
603
- * @fires save-settings to AppCompSettingsMenu.vue
779
+ * @description Handle the content of the branch page to display in the right sidebar.
780
+ * @summary When call set the value of compID and the branching visibility Attach lister for scroll event in the sidebar
781
+ * @param {Object} branchID - ID OF the Component That to retrieve
782
+ * @fires 'branch-page-viewed' to $PageMixins when scroll in branch page reaches the bottom
604
783
  */
605
- onSplashClose() {
606
- //set autoplay to false by default when user does not make a choice and close splash-screen.
607
- if (this.settingsSelected.autoplay === null)
608
- this.settingsSelected.autoplay = false
784
+ openBranchContent(branchID) {
785
+ this.branchingVisible = true
786
+ this.compID = branchID //set compenent ID
787
+ setTimeout(() => {
788
+ const rightSidebar = this.getRightSidebar()
789
+ //Should indicate that page is completed when the Rightsidebar content heigh is less then window height
790
+ if (rightSidebar.scrollHeight <= window.innerHeight) {
791
+ if (!this.getCurrentBranchPage) return
792
+ let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
793
+ this.getCurrentBranchPage.state = 'completed' // update the state of this branch
794
+ userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
795
+ this.$bus.$emit('branch-page-viewed')
796
+ }
609
797
 
610
- //set on boarding to false by default if it is defined and its value is null
611
- if (
612
- typeof this.settingsSelected.onboarding !== 'undefined' &&
613
- this.settingsSelected.onboarding === null
614
- )
615
- this.settingsSelected.onboarding = false
798
+ rightSidebar.addEventListener('scroll', this.handleRightSidebarScroll)
799
+ }, 300)
800
+ },
801
+
802
+ /**
803
+ * @description Close the branch page in the right sidebar
804
+ * and update the current page data with the parent of the branch page
805
+ *
806
+ * @summary When call remove listner for scroll event in the sidebar , reset compID and branch visibility
807
+ * @fires 'branching-hidden' to AppCompButtonProgress
808
+ */
809
+ closeBranchContent() {
810
+ const rightSidebar = this.getRightSidebar()
811
+
812
+ if (!rightSidebar) return
616
813
 
617
- this.$bus.$emit('save-settings', this.settingsSelected) //save to db
814
+ rightSidebar.removeEventListener('scroll', this.handleRightSidebarScroll)
815
+
816
+ setTimeout(() => {
817
+ this.compID = null //reset the comp
818
+ this.branchingVisible = false
819
+ }, 300)
820
+ this.$bus.$emit('branching-hidden')
821
+ this.updateContent(this.$route) // update the content of the current page
822
+ },
823
+
824
+ displayCustomContent(c, container) {
825
+ if (!c) return (this.customContentVisible = false)
826
+ //Change transcript content
827
+ if (this.transcriptVisible) this.closeTranscript()
828
+ //Change transcript content
829
+ this.customContent = c
830
+ this.customContentContainer = container
831
+ this.customContentVisible = true
832
+ },
833
+
834
+ closeCostumContent() {
835
+ this.customContentContainer = null
836
+ this.customContentVisible = false
618
837
  },
619
838
 
620
839
  /**
621
840
  * @description reset the values of state (currentTimeline,currentMedialement, currentPage, media duration, appStatus')in the store
622
841
  */
623
842
  async unload() {
843
+ if (!this.getCurrentPage || Object.keys(this.getCurrentPage).length == 0)
844
+ return
845
+
624
846
  return new Promise((res) => {
625
- this.$store.dispatch('updateCurrentTimeline', '')
626
- this.$store.dispatch('updateCurrentMediaElement', '')
627
- this.$store.dispatch('updateCurrentPage', {})
628
- this.updateTracker('AppBaseModule', 'loading')
629
- res(this.$store.state.$appStore.compStatusTracker)
847
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
848
+ this.updateCurrentTimeline(null)
849
+ this.updateCurrentMediaElements([])
850
+ // this.updateCurrentPage({})
851
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
852
+ res(this.getCompStatusTracker)
630
853
  })
631
854
  },
632
855
 
633
- /**
634
- * @description fetch a page from the store
635
- */
636
- async fetchPage() {
637
- const fetched = await new Promise((resolve) => {
638
- // get current page after 200 milliseconds
639
- this.mediaToShw = false
640
- setTimeout(() => {
641
- resolve(this.getCurrentPage)
642
- }, 200)
643
- })
644
- return fetched
645
- },
646
856
  /**
647
857
  * @description get User data
648
858
  */
@@ -692,92 +902,44 @@ export default {
692
902
  }
693
903
  } else return false
694
904
  },
905
+ /**
906
+ * @description Initialize the lesson
907
+ * - check the package type and launch the lesson accordingly
908
+ * - update the content of the current page in the store with data of the page requested
909
+ */
695
910
  async initLesson() {
696
- await this.unload().then(() => {
697
- const packageType = this.getModuleInfo.packageType
698
- switch (packageType) {
699
- case 'scorm': {
700
- // launching the lesson (if the lesson is not already completed)
701
- const lessonStatus = this.$scorm.GetValue(
702
- 'cmi.core.lesson_status',
703
- true
704
- )
705
- if (lessonStatus === 'unknown') {
706
- this.$scorm.setValue('cmi.core.lesson_status', 'incomplete') // set the lesson status to in complete
707
- this.$scorm.Commit() // persist data
708
- }
709
-
710
- // check for the bookmark existance in LMS
711
- const bookmark = this.$scorm.GetValue(
712
- 'cmi.core.lesson_location',
713
- false
714
- )
715
-
716
- // if none stored in the LMS redirect to the 1st page
717
- if (
718
- !this.bookmarkActive ||
719
- bookmark === '' ||
720
- bookmark === undefined
721
- ) {
722
- //Redirect to current page or to menu if route is module
723
- this.$route.name && this.$route.name !== 'module'
724
- ? this.$router.push({ name: this.$route.name })
725
- : this.$router.push({ name: 'menu' })
726
- } else if (this.bookmarkActive && bookmark) {
727
- this.$router.push({ name: bookmark })
728
- }
729
- // set the current page
730
- this.fetchPage().then((res) => {
731
- this.mediaToShw = this.formateData(res)
732
- this.updateTracker('AppBaseModule', 'ready')
733
- })
734
- this.$scorm.getScormAPI()
735
- break
911
+ if (this.lessonStarted) return
912
+
913
+ const packageType = this.getModuleInfo.packageType
914
+ switch (packageType) {
915
+ case 'scorm': {
916
+ // launching the lesson (if the lesson is not already completed)
917
+ const lessonStatus = this.$scorm.GetValue(
918
+ 'cmi.core.lesson_status',
919
+ true
920
+ )
921
+ if (lessonStatus === 'unknown') {
922
+ this.$scorm.setValue('cmi.core.lesson_status', 'incomplete') // set the lesson status to in complete
923
+ this.$scorm.Commit() // persist data
736
924
  }
737
- case 'xapi':
738
- {
739
- //Check if there is a user connection
740
- if (
741
- this.getConnectionInfo &&
742
- this.getConnectionInfo.actor &&
743
- this.getConnectionInfo.remote
744
- ) {
745
- // check for the bookmark existance in LMS
746
- const bookmark = this.$xapi._getLessonPosition(
747
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
748
- this.getConnectionInfo.activity_id
749
- )[0]
750
- // if none stored in the LMS redirect to the 1st page
751
- if (
752
- !this.bookmarkActive ||
753
- bookmark === '' ||
754
- bookmark === undefined
755
- ) {
756
- //Redirect to current page or to menu if route is module
757
- this.$route.name && this.$route.name !== 'module'
758
- ? this.$router.push({ name: this.$route.name })
759
- : this.$router.push({ name: 'menu' })
760
- } else if (this.bookmarkActive && bookmark) {
761
- // go to last reached page
762
- this.$router.push({ name: bookmark })
763
- }
764
- } else {
765
- //Redirect to current page or to menu if route is module
766
-
767
- this.$route.name && this.$route.name !== 'module'
768
- ? this.$router.push({ name: this.$route.name })
769
- : this.$router.push({ name: 'menu' })
770
- }
771
- // set the current page
772
- this.fetchPage().then((res) => {
773
- this.mediaToShw = this.formateData(res)
774
- this.updateTracker('AppBaseModule', 'ready')
775
- })
776
- }
777
- break
925
+ //Redirect to current page or to menu if route is module
926
+ this.$route.name && this.$route.name !== 'module'
927
+ ? this.$router.push({ name: this.$route.name })
928
+ : this.$router.push({ name: 'menu' })
929
+ break
778
930
  }
779
- })
931
+ case 'xapi':
932
+ {
933
+ this.$route.name && this.$route.name !== 'module'
934
+ ? this.$router.push({ name: this.$route.name })
935
+ : this.$router.push({ name: 'menu' })
936
+ }
937
+ break
938
+ }
939
+ this.lessonStarted = true
940
+ await this.updateContent(this.$route)
780
941
  },
942
+ //=============================================================================
781
943
 
782
944
  /**
783
945
  * @description save interaction or Module state to scrom suspend_data
@@ -787,302 +949,70 @@ export default {
787
949
  arg = arg || null
788
950
  let existingRecord // hold record record
789
951
  let toBeSaved // hold data to send to scorm
952
+
790
953
  if (this.$scorm.initialized) {
791
954
  if (this.$scorm.GetValue('cmi.suspend_data') !== '')
792
955
  existingRecord = JSON.parse(this.$scorm.GetValue('cmi.suspend_data')) // try convert the scorm record to JSON Obiect
793
956
  // create entry for user data if there is no record in Scorm
794
957
  if (!existingRecord) existingRecord = {}
795
- // There is a passed arg
796
- if (arg !== null) {
797
- // update only the user record
798
- if (arg === 'userInteraction') {
799
- // create new entry for user data if does not existe
800
- if (!existingRecord['userData']) existingRecord['userData'] = {}
801
- // update value of user data
802
- existingRecord.userData = this.getUserInteraction
803
- toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
804
- }
805
- // update only the module record
806
- else if (arg === 'module') {
807
- // create new entry for module data if does not existe
808
- if (!existingRecord['moduleData']) existingRecord['moduleData'] = {}
809
- // update value for module data
810
- toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
811
- }
812
- // no valid arg
813
- else return
814
- }
815
- // Update module & user record
816
- else {
817
- // create entry for module and user data if does not existe
818
- if (
819
- !existingRecord['userData'] &&
820
- !existingRecord['moduleData'] &&
821
- !existingRecord['routeHistory']
822
- ) {
823
- existingRecord['moduleData'] = {}
824
- existingRecord['userData'] = {}
825
- existingRecord['routeHistory'] = []
826
- }
827
- // update values for module and user data
828
- existingRecord.userData = this.getUserInteraction
829
- existingRecord.routeHistory = this.routeHistory
830
- // update value of user data
831
- existingRecord.moduleConfig = JSON.stringify({}) // update value of module data
832
- toBeSaved = JSON.stringify(existingRecord)
833
- }
958
+
959
+ // create new entry for user data if does not existe
960
+ existingRecord.userData = this.getUserInteraction || {}
961
+ existingRecord.userSettings = this.getApplicationSettings || {}
962
+ existingRecord.routeHistory = (() => {
963
+ const history = this.getRouteHistory.toReversed() //get the route history from the last
964
+ history[0] = this.$route.meta // change the last recored route to the current route
965
+ return history.toReversed()
966
+ })()
967
+ // update value of user data
968
+ toBeSaved = JSON.stringify(existingRecord) // convert to JSON string format
969
+
834
970
  this.$scorm.SetValue('cmi.suspend_data', toBeSaved) // converte to serialized string and save to scorm
835
971
  this.$scorm.Commit() // persist data in LMS
972
+ if (arg === 'disconnect') this.$scorm.Finish()
836
973
  }
837
974
  },
838
975
  /**
839
- * @param {Object} to
840
- * @param {Object} from unused
841
- * @param {Function} next
976
+ * @description update content when route change or a page change is requested
977
+ * - update the information of the current page in the store with data of the page requested
978
+ *
979
+ * @param {Object} to destination route
842
980
  */
843
- updateContent(to, from, next) {
981
+ async updateContent(to, caller = null) {
982
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'loading')
844
983
  if (this.getModuleInfo.packageType === 'scorm')
845
984
  this.saveToScorm('userInteraction') //save to scorm
846
985
 
847
- if (this.popupIsOpen) {
848
- //close popup and deactivate the popup animation
849
- this.closePopup({ animationOff: true })
850
- }
851
-
852
- this.unload().then((res) => {
853
- if (res.length) {
854
- //======== Handle redirect to correct path when user enter incorrect path for existing route ====//
855
- let toName = null
856
- if (!to.name) {
857
- toName = to.fullPath.replace(/\//g, ' ').trim() //clean route name
986
+ await this.unload()
987
+ const { activity_ref: activity_Id, id: page_Id } = to.meta ? to.meta : to
858
988
 
859
- //apply apply correct format for routes that concerne an activity
860
- if (toName.includes('activite-')) {
861
- toName = toName.split(' ')[0].replace('-', '_')
862
- }
863
- //Go to menu when introactive is false other wise will navigate to introduction
864
- else if (
865
- toName.includes('introduction') &&
866
- !this.theIntroIsActivated
867
- )
868
- toName = 'menu'
869
- next({ name: toName }) //navigate to correct path
870
- }
871
- //When User inter path to module
872
- else if (to.name === 'module') {
873
- //Intro is not active set navigation path to menu
874
- //if (!this.theIntroIsActivated) toName = 'menu'
875
- //Intro active set navigation path to introduction
876
- toName = 'menu'
877
- next({ name: toName }) //navigate to correct path
878
- }
879
- //Default redirection
880
- else {
881
- next()
882
- }
883
- //===================== Handeling a page request from Store=========================== ====//
884
- //get the page from store
885
- this.fetchPage().then((res) => {
886
- this.mediaToShw = this.formateData(res)
887
- this.updateTracker('AppBaseModule', 'ready')
888
- //Save current page has bookmark in scorm
889
- if (
890
- this.getModuleInfo.packageType === 'scorm' &&
891
- this.$scorm.initialized &&
892
- this.bookmarkActive
893
- )
894
- this.$scorm.setBookMark(this.$route.name)
895
- })
896
- }
897
- })
989
+ await this.updateCurrentPage({ activity_Id, page_Id }, caller)
990
+ this.$bus.$emit('set-comp-status', 'appBaseModule', 'ready')
898
991
  },
899
992
 
900
993
  /**
901
- * @description Send a custom statement to the lrs. Receive some custom params and build a statement to send
902
- * @param {Object} stmtObj
903
- * @param {Function} cb
994
+ * @description update the route history when navigation happen
995
+ * @param {Object} from - origin route
996
+ *
904
997
  */
905
- sendXapiStatement(stmtObj, cb = null, withFetch = false) {
906
- cb = cb || null
907
- const crsParams = this.getModuleInfo
908
-
909
- if (
910
- this.getConnectionInfo &&
911
- this.getConnectionInfo.actor &&
912
- this.getConnectionInfo.remote
913
- ) {
914
- const {
915
- id,
916
- result = null,
917
- definition,
918
- objectType,
919
- type,
920
- description,
921
- verb,
922
- extension,
923
- duration,
924
- completion,
925
- } = stmtObj
926
- //unload
927
- //define the activity id
928
- let activityId
929
- //=========================== activity ID of Object ==========================================
930
- /*
931
- * Define the acitivity id of the stmt according to following:
932
- * The statement is sent at component/ element level : there is Id and Id is not null (id of the element)
933
- * The statement is sent at module level. there is no id
934
- * the Statement must be sent at the course level: There course_id exist and id is the course_id
935
- */
936
-
937
- if (id && id !== crsParams.courseID)
938
- activityId = `${this.getConnectionInfo.activity_id}/${id}`
939
- else if (crsParams.courseID && id === crsParams.courseID)
940
- activityId = this.getConnectionInfo.activity_id.replace(
941
- `/${crsParams.id}`,
942
- ''
943
- )
944
- else activityId = `${this.getConnectionInfo.activity_id}`
945
-
946
- //define the statement object
947
- let stmt = {
948
- actor: this.getConnectionInfo.actor,
949
- verb: (() => {
950
- if (verb && this.$xapi.verbs[verb.trim()])
951
- return this.$xapi.verbs[verb.trim()]
952
- else {
953
- //check if the agent exist in the learning record for this activity
954
- // to determine wether he is lunching the activity or resuming the activity
955
- if (
956
- this.$xapi._getAgent(
957
- this.getConnectionInfo.actor.mbox.replace('mailto:', ''),
958
- activityId
959
- )
960
- )
961
- return this.$xapi.verbs.resumed
962
- else return this.$xapi.verbs.started
963
-
964
- }
965
- })(),
966
- object: {
967
- id: activityId,
968
- definition: {
969
- name: {
970
- [this.displayLang]: `${definition}`
971
- },
972
- description: {
973
- [this.displayLang]: `${description}`,
974
- type: type || 'http://activitystrea.ms/schema/1.0/page'
975
- }
976
- },
977
- objectType: objectType || 'Activity'
978
- },
979
- context: {
980
- contextActivities: {}
981
- }
982
- }
983
- //===================== contextActivity parent =====================//
984
- /*
985
- Define parent in the contextActivity
986
- When:
987
- 1- when we have the id (parent === module)
988
- 2- when with have no id but we have a course_id (parent is cours_id)
989
- */
990
- const activityParent = (() => {
991
- if (crsParams.courseID && !id)
992
- return {
993
- id: this.getConnectionInfo.activity_id.replace(
994
- `/${crsParams.id}`,
995
- ''
996
- ),
997
- objectType: objectType || 'Activity'
998
- }
999
- else if (id && id !== crsParams.courseID)
1000
- return {
1001
- id: `${this.getConnectionInfo.activity_id}`,
1002
- objectType: objectType || 'Activity'
1003
- }
1004
- else return null
1005
- })()
1006
-
1007
- // Add the parent key of the context activity
1008
- if (activityParent)
1009
- stmt.context.contextActivities['parent'] = [activityParent]
998
+ updateRouteHistory(from) {
999
+ //The route is not in the history: should be push at the end of the history
1000
+ const targetIndex = this.routeData.findIndex((r) => {
1001
+ if (r.type === 'pg_menu' && r.id === from.meta.id) return r
1002
+ else if (
1003
+ `${r.activity_ref}_${r.id}` ===
1004
+ `${from.meta.activity_ref}_${from.meta.id}`
1005
+ )
1006
+ return r
1007
+ })
1010
1008
 
1011
- //===================== contextActivity grouping =====================//
1009
+ //Remove route from history if already in
1010
+ if (targetIndex !== -1) this.routeData.splice(targetIndex, 1)
1012
1011
 
1013
- //Defining the Grouping of the context activity
1014
- const activityGrouping = (() => {
1015
- if (
1016
- activityParent &&
1017
- crsParams.courseID &&
1018
- activityParent.id.includes(crsParams.id)
1019
- )
1020
- return {
1021
- id: this.getConnectionInfo.activity_id.replace(
1022
- `/${crsParams.id}`,
1023
- ''
1024
- )
1025
- }
1026
- else return null
1027
- })()
1028
- //Adding the grouping of the context activity
1029
- if (activityGrouping)
1030
- stmt.context.contextActivities['grouping'] = [activityGrouping]
1031
-
1032
- //add result data to statement
1033
- if (result) stmt['result'] = result
1034
-
1035
- // Add duration info to statment when activity is complete
1036
- if (verb === 'completed' || verb === 'suspended' || verb === 'exited') {
1037
- if (!stmt.result) stmt.result = {} // check if exist
1038
-
1039
- let d
1040
-
1041
- duration
1042
- ? (d = `PT${duration.split(':')[0]}H${duration.split(':')[1]}M${
1043
- duration.split(':')[2]
1044
- }S`)
1045
- : (d = `PT${this.activityDuration.split(':')[0]}H${
1046
- this.activityDuration.split(':')[1]
1047
- }M${this.activityDuration.split(':')[2]}S`)
1048
-
1049
- stmt.result['duration'] = d
1050
- //Set the completion status of the result
1051
- if (completion) stmt.result['completion'] = completion
1052
- }
1012
+ // Add route route in history
1013
+ this.routeData.push(from.meta)
1053
1014
 
1054
- // ===================== Extension of the Object definition =====================//
1055
- if (
1056
- extension &&
1057
- extension.constructor === Array &&
1058
- extension.length > 0
1059
- ) {
1060
- //Validate each entry given in the extension Array
1061
- extension.forEach((e) => {
1062
- //entry must be of type Object
1063
- if (e.constructor !== Object)
1064
- throw new Error(`'${e}' is not a valid value. Must be Object`)
1065
-
1066
- //Entry Must have id and content keys
1067
- const validKey = ['id', 'content']
1068
- Object.keys(e).forEach((key) => {
1069
- if (!validKey.includes(key))
1070
- throw new Error(`Not valid key '${key}' for entry ${e}`)
1071
-
1072
- //id must be a String
1073
- if (key === 'id' && e[key] && e[key].constructor !== String)
1074
- throw new Error(`'${key}' must be of type String`)
1075
- })
1076
-
1077
- stmt.object.definition['extensions'] = {
1078
- ...stmt.object.definition['extensions'],
1079
- [`${this.getConnectionInfo.activity_id}/${e.id}`]: e.content
1080
- }
1081
- })
1082
- }
1083
-
1084
- this.$xapi._sendStatement(stmt, cb, withFetch)
1085
- }
1015
+ if (this.routeData.length >= 4) this.routeData.shift()
1086
1016
  },
1087
1017
 
1088
1018
  /**
@@ -1111,68 +1041,12 @@ export default {
1111
1041
  res.url = `${baseDomain}/${res.url}`
1112
1042
 
1113
1043
  const newUrlToLaunch = `${res.url}?endpoint=${endpoint}&auth=${auth}&actor=${actor}&registration=${registration}&activity_id=${activity_id}`
1114
-
1115
- this.endLesson(() => {
1044
+ this.routeChangeCounter = 0 //reset counter after saving
1045
+ this.$bus.$emit('fire-exit-event', () => {
1116
1046
  window.location.replace(newUrlToLaunch)
1117
1047
  })
1118
1048
  },
1119
1049
 
1120
- /**
1121
- * @param {Function} cb
1122
- * @param {Bool} option - set if must use Fetch API or not. default = false
1123
- */
1124
- endLesson(cb, option = false) {
1125
- let text
1126
-
1127
- //Defining the text to display for stmt description and definition
1128
- switch (this.$i18n.locale) {
1129
- case 'fr':
1130
- if (this.getModuleInfo.courseID)
1131
- text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
1132
- else text = `Le ${this.getModuleInfo.id}`
1133
- break
1134
- case 'en':
1135
- if (this.getModuleInfo.courseID)
1136
- text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
1137
- else text = `The ${this.getModuleInfo.id}`
1138
- break
1139
- }
1140
-
1141
- // Retrieve only user data in lessons front its interaction that we want to send to the LRS.
1142
- // Note: User Settings are sent on a different URI and statement
1143
- const {
1144
- isFistTime,
1145
- userSettings,
1146
- ...lessonsData
1147
- } = this.getUserInteraction
1148
-
1149
- const stmt = {
1150
- verb: 'exited',
1151
- definition: text,
1152
- description: text,
1153
- extension: [
1154
- {
1155
- id: 'ending-point',
1156
- content: this.$route.name
1157
- },
1158
- {
1159
- id: 'user-data',
1160
- content: {
1161
- routeHistory: this.getRouteHistory,
1162
- ...lessonsData
1163
- }
1164
- }
1165
- ],
1166
- duration: this.lessonDuration
1167
- }
1168
-
1169
- this.sendXapiStatement(stmt, null, option)
1170
-
1171
- if (this.timerState === 'started') setTimeout(() => this.stopTimer(), 0) //clear the timer
1172
-
1173
- if (cb) cb()
1174
- },
1175
-
1176
1050
  /**
1177
1051
  * @description start the tutorial for first time users
1178
1052
  * @requires '../assets/data/onboardingMessages.json'
@@ -1188,13 +1062,17 @@ export default {
1188
1062
  this.nextOnboarding('message_1')
1189
1063
  } catch (e) {
1190
1064
  //fetch default values for popups
1191
- this.onboardingMessages = require('../assets/data/onboardingMessages.json')
1065
+ this.onboardingMessages = import(
1066
+ '../assets/data/onboardingMessages.json'
1067
+ )
1192
1068
  this.nextOnboarding('message_1')
1193
1069
  }
1194
1070
  }
1195
1071
  } else {
1196
1072
  if (this.getOnboardingEnabled) {
1197
- this.onboardingMessages = require('../assets/data/onboardingMessages.json')
1073
+ this.onboardingMessages = import(
1074
+ '../assets/data/onboardingMessages.json'
1075
+ )
1198
1076
  this.nextOnboarding('message_1')
1199
1077
  }
1200
1078
  }
@@ -1219,8 +1097,6 @@ export default {
1219
1097
  this.toolTipTarget = messages[postNextMessage].value.target
1220
1098
  }
1221
1099
  }
1222
-
1223
- //check if next message exist
1224
1100
  //check if the next message is a popup or other componant
1225
1101
  if (messages[nextMessage].type == 'popup-avert') {
1226
1102
  this.openPopup(messages[nextMessage])
@@ -1232,29 +1108,22 @@ export default {
1232
1108
  * @description- Skip directly to the main containt*
1233
1109
  * main content is Node with page ID by default
1234
1110
  * Main Content can be defined by front-end using prop "main" in Module.vue of project
1111
+ * @fire {event} 'move-to-target' to AppBase
1235
1112
  */
1236
1113
 
1237
1114
  skipToMain() {
1238
1115
  const { main } = this.moduleConfig // check for main input
1239
- let skipTo = document.querySelector(`#wrapper-content`) //default definition of main element
1240
-
1241
- if (main) {
1242
- let targetEl = document.querySelector(`#${main}`) // search for node element specified as main
1243
1116
 
1244
- if (targetEl) skipTo = targetEl
1245
- }
1246
- let targetTop = skipTo.offsetTop
1117
+ let skipTo = main ? main : 'wrapper-content' // search for node element specified as main
1247
1118
 
1248
- let scrollOpt = {
1249
- top: targetTop - 100,
1119
+ //fires event
1120
+ this.$bus.$emit('move-to-target', skipTo, {
1121
+ top: 100, // offset target top to 100 pixel
1250
1122
  left: 0,
1251
- behavior: 'auto' //auto
1252
- }
1123
+ behavior: 'auto'
1124
+ })
1253
1125
 
1254
- //Allowing accessibility control with keyboard
1255
- skipTo.setAttribute('tabIndex', -1)
1256
- window.scrollTo(scrollOpt)
1257
- skipTo.focus()
1126
+ this.$analytics.sendEvent('skip_to_main_content')
1258
1127
  },
1259
1128
 
1260
1129
  /**
@@ -1266,13 +1135,11 @@ export default {
1266
1135
  let err = false
1267
1136
  let errorList = [] //array for errors dectected
1268
1137
  if (onboardingMessages && typeof onboardingMessages == 'object') {
1269
- //continue
1270
1138
  //check the list of attributes is message_X
1271
1139
  let listMessage = Object.keys(onboardingMessages)
1272
1140
  for (let index = 1; index <= listMessage.length; index++) {
1273
1141
  const element = listMessage[index - 1]
1274
1142
  if (element == 'message_' + index) {
1275
- //continu
1276
1143
  if (
1277
1144
  onboardingMessages[element] &&
1278
1145
  typeof onboardingMessages[element] == 'object'
@@ -1293,7 +1160,7 @@ export default {
1293
1160
  //flags error
1294
1161
  errorList.push('message_' + index + ' type value title')
1295
1162
  console.warn(
1296
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1163
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1297
1164
  index +
1298
1165
  ' type value title is incorrect',
1299
1166
  'background: orange; color: white; display: block; margin:5px;'
@@ -1307,7 +1174,7 @@ export default {
1307
1174
  //flags error
1308
1175
  errorList.push('message_' + index + ' type value content')
1309
1176
  console.warn(
1310
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1177
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1311
1178
  index +
1312
1179
  ' type value content is incorrect',
1313
1180
  'background: orange; color: white; display: block; margin:5px;'
@@ -1327,7 +1194,7 @@ export default {
1327
1194
  'message_' + index + ' type value target'
1328
1195
  )
1329
1196
  console.warn(
1330
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1197
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1331
1198
  index +
1332
1199
  ' type value target is not found in page',
1333
1200
  'background: orange; color: white; display: block; margin:5px;'
@@ -1337,7 +1204,7 @@ export default {
1337
1204
  //flags error
1338
1205
  errorList.push('message_' + index + ' type value target')
1339
1206
  console.warn(
1340
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1207
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1341
1208
  index +
1342
1209
  ' type value target is incorrect',
1343
1210
  'background: orange; color: white; display: block; margin:5px;'
@@ -1347,7 +1214,7 @@ export default {
1347
1214
  //flags error
1348
1215
  errorList.push('message_' + index + ' type value')
1349
1216
  console.warn(
1350
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1217
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1351
1218
  index +
1352
1219
  ' type value is incorrect',
1353
1220
  'background: orange; color: white; display: block; margin:5px;'
@@ -1358,7 +1225,7 @@ export default {
1358
1225
  //flags error
1359
1226
  errorList.push('message_' + index + ' type')
1360
1227
  console.warn(
1361
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1228
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1362
1229
  index +
1363
1230
  ' type is missing',
1364
1231
  'background: orange; color: white; display: block; margin:5px;'
@@ -1368,7 +1235,7 @@ export default {
1368
1235
  //flags error
1369
1236
  errorList.push('message_' + index + ' typeof')
1370
1237
  console.warn(
1371
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages message_' +
1238
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages message_' +
1372
1239
  index +
1373
1240
  ' is not an object',
1374
1241
  'background: orange; color: white; display: block; margin:5px;'
@@ -1378,7 +1245,7 @@ export default {
1378
1245
  //flags error
1379
1246
  errorList.push('onboardingMessages keys')
1380
1247
  console.warn(
1381
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages keys does not follow "message_[number]" naming',
1248
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages keys does not follow "message_[number]" naming',
1382
1249
  'background: orange; color: white; display: block; margin:5px;'
1383
1250
  )
1384
1251
  }
@@ -1387,7 +1254,7 @@ export default {
1387
1254
  //flags error
1388
1255
  errorList.push('onboardingMessages')
1389
1256
  console.warn(
1390
- '%c WARNING!>>> AppBaseModule: Your onboardingMessages is not an Object',
1257
+ '%c WARNING!>>> appBaseModule: Your onboardingMessages is not an Object',
1391
1258
  'background: orange; color: white; display: block; margin:5px;'
1392
1259
  )
1393
1260
  }
@@ -1398,192 +1265,370 @@ export default {
1398
1265
 
1399
1266
  return err
1400
1267
  },
1401
- updateTracker(name, status) {
1402
- this.$store.dispatch('updateCompStatusTracker', { name, status })
1403
- }
1404
- }
1405
- }
1406
- </script>
1407
- <style lang="scss">
1408
- $heigthNavbar: 55px;
1409
1268
 
1410
- #App-base {
1411
- .skip-link {
1412
- left: -999px;
1413
- position: absolute;
1414
- top: auto;
1415
- width: 1px;
1416
- height: 1px;
1417
- overflow: hidden;
1418
- z-index: -999;
1419
-
1420
- &:focus,
1421
- &:active {
1422
- left: auto;
1423
- top: auto;
1424
- width: 30%;
1425
- height: auto;
1426
- overflow: auto;
1427
- margin: 10px 35%;
1428
- padding: 5px;
1429
- text-align: center;
1430
- font-size: 1.2em;
1431
- z-index: 999;
1432
- }
1433
- }
1434
- /********** FM **********/
1435
- #Loading {
1436
- width: 100%;
1437
- position: fixed;
1438
- z-index: 99999;
1439
- left: 0;
1440
- opacity: 0;
1441
- display: none;
1442
- transition: opacity 0.5s;
1443
- pointer-events: none;
1444
- }
1269
+ /**
1270
+ * @description Reset the focus the element
1271
+ * @param {HTMLElement} e - element that will get focus
1272
+ */
1273
+ resetFocus(e) {
1274
+ if (e) e.focus()
1275
+ },
1445
1276
 
1446
- &:not(.DR) {
1447
- /******** loading *******/
1448
- #Loading {
1449
- top: 0px;
1450
- height: 100%;
1451
- }
1452
- #primary_nav_wrapper {
1453
- position: fixed;
1454
- right: 0;
1455
- bottom: 95px;
1456
- z-index: 100;
1457
- pointer-events: none;
1458
-
1459
- button {
1460
- pointer-events: all;
1461
- }
1462
- }
1463
- .app-nav {
1464
- position: relative;
1465
- z-index: 10;
1466
- width: 100%;
1467
- height: $heigthNavbar;
1468
- padding: 0;
1469
- justify-content: center;
1470
-
1471
- @media screen and (max-width: 1100px) {
1472
- position: relative;
1473
- top: 0;
1474
- transition: top 0.5s ease-out;
1475
-
1476
- &.show {
1477
- top: -65px;
1478
- }
1479
- }
1277
+ handleRightSidebarScroll(event) {
1278
+ let scrollHeight = null
1279
+ let clientHeight = null
1280
+ let scrollTop = null
1480
1281
 
1481
- #ActivityMenu {
1482
- top: inherit;
1483
- bottom: $heigthNavbar;
1484
- }
1485
- }
1486
- }
1487
- /********** DR **********/
1488
- &.DR {
1489
- #Loading {
1490
- top: $heigthNavbar;
1491
- //height: calc($heigthNavbar - 100%);
1492
- }
1493
- .app-nav {
1494
- position: fixed;
1495
- top: 0;
1496
- z-index: 10;
1497
- width: 100%;
1498
- height: $heigthNavbar;
1499
- padding: 0;
1500
- justify-content: center;
1501
-
1502
- #ActivityMenu {
1503
- top: $heigthNavbar;
1504
- bottom: inherit;
1282
+ scrollHeight = event.target.scrollHeight
1283
+ clientHeight = event.target.clientHeight
1284
+ scrollTop = event.target.scrollTop
1285
+
1286
+ // //Set scroll limit reached at 150px above the document height.
1287
+ let scrollLimit = scrollHeight - 150
1288
+ let fullyScrolled = Math.round(clientHeight + scrollTop)
1289
+
1290
+ //consider page completed when scrolled value has reached or passed set limit
1291
+ if (fullyScrolled >= scrollLimit) {
1292
+ event.target.removeEventListener(
1293
+ 'scroll',
1294
+ this.handleRightSidebarScroll
1295
+ )
1296
+ let { userInteraction } = this.getCurrentBranchPage // Get the current branch page opened in the sidebar
1297
+
1298
+ this.getCurrentBranchPage.state = 'completed' // update the state of this branch
1299
+ userInteraction.state = this.getCurrentBranchPage.state //update userInteraction state
1300
+
1301
+ setTimeout(() => this.$bus.$emit('branch-page-viewed'), 20)
1505
1302
  }
1506
- #menu {
1507
- margin-right: 20px;
1303
+ },
1304
+ getRightSidebar() {
1305
+ let rSidebar = null
1306
+ rSidebar = document.getElementById('right-sidebar')
1307
+ if (!rSidebar) return
1308
+ const rSidebarContent = rSidebar.querySelector(`#right-sidebar-body`)
1309
+ return rSidebarContent
1310
+ },
1311
+
1312
+ startTimeout() {
1313
+ this.timeOut = setTimeout(() => {
1314
+ this.cancelTimeout()
1315
+ }, 3000)
1316
+ },
1317
+ cancelTimeout() {
1318
+ if (this.timeOut) {
1319
+ this.closeDelay = false
1320
+ clearTimeout(this.timeOut)
1508
1321
  }
1322
+ },
1323
+
1324
+ /**
1325
+ * @description Method to handle to sanding of completion status to LRS
1326
+ * determine wether to send a completion for an activity or the Lesson
1327
+ * Completion is sent once
1328
+ * @param {String} context - 'ACTIVITY | LESSON'
1329
+ */
1330
+
1331
+ sendCompletionStatus(context = null) {
1332
+ if (
1333
+ !this.getModuleInfo.packageType === 'xapi' ||
1334
+ !this.getConnectionInfo ||
1335
+ !this.getConnectionInfo.actor ||
1336
+ !this.getConnectionInfo.remote ||
1337
+ !context
1338
+ )
1339
+ return
1340
+
1341
+ let stmt = null
1342
+
1343
+ switch (context) {
1344
+ case 'ACTIVITY': {
1345
+ let text
1346
+ let completedSize = 0
1347
+ Object.entries(this.getAllCompleted).forEach((value) => {
1348
+ completedSize = completedSize + value[1].length
1349
+ })
1350
+ let recordInServerLength,
1351
+ thisActivityProgressLength,
1352
+ thisActivityLength
1509
1353
 
1510
- .app-ctrl {
1511
- display: flex;
1512
- align-items: center;
1513
- justify-content: center;
1514
- width: 100%;
1515
- height: 100%;
1516
- .btn-base {
1517
- margin: 10px;
1354
+ const thisActivityServerState =
1355
+ this.getUserInteraction[this.$route.meta.activity_ref]
1356
+
1357
+ if (thisActivityServerState)
1358
+ recordInServerLength = Object.keys(thisActivityServerState).length
1359
+
1360
+ thisActivityProgressLength = this.getAllCompleted[
1361
+ this.$route.meta.activity_ref
1362
+ ]
1363
+ ? this.getAllCompleted[this.$route.meta.activity_ref].length
1364
+ : 0
1365
+
1366
+ //get The total length of the current activity
1367
+ thisActivityLength = this.getAllActivities(
1368
+ this.getCurrentPage.activityRef
1369
+ ).pageSize
1370
+
1371
+ if (thisActivityProgressLength !== thisActivityLength) return
1372
+ if (thisActivityProgressLength == recordInServerLength) return
1373
+
1374
+ //Defining the text to display for stmt description and definition
1375
+ const id = this.getCurrentPage.activityRef
1376
+ let aName = ''
1377
+ switch (true) {
1378
+ case id == 'A00':
1379
+ aName = 'Introduction'
1380
+ break
1381
+ case id == 'A99':
1382
+ aName = 'Conclusion'
1383
+ break
1384
+
1385
+ default: {
1386
+ let d = id.replace('A', '').trim()
1387
+ d = parseInt(d)
1388
+ aName = `${this.$t('text.activity')} ${d}`
1389
+ }
1390
+ }
1391
+
1392
+ switch (this.$i18n.locale) {
1393
+ case 'fr':
1394
+ if (this.getModuleInfo.courseID)
1395
+ text = `${aName} de ${this.getModuleInfo.id} `
1396
+ else text = `Le ${this.getModuleInfo.id}`
1397
+ break
1398
+ case 'en':
1399
+ if (this.getModuleInfo.courseID)
1400
+ text = `${aName} of ${this.getModuleInfo.id}`
1401
+ else text = `The ${this.getModuleInfo.id}`
1402
+ break
1403
+ }
1404
+
1405
+ // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1406
+ // Note: User Settings are sent on a different URI and statement
1407
+ const { isFistTime, userSettings, ...lessonsData } =
1408
+ this.getUserInteraction
1409
+ const activityDuration = this.appTimer.getTime()
1410
+ stmt = {
1411
+ id: this.getCurrentPage.activityRef,
1412
+ verb: 'completed',
1413
+ definition: text,
1414
+ description: text,
1415
+ extensions: [
1416
+ {
1417
+ id: 'user-data',
1418
+ content: {
1419
+ routeHistory: this.getRouteHistory,
1420
+ ...lessonsData
1421
+ }
1422
+ }
1423
+ ],
1424
+ duration: this.appTimer.ISOTimeParser(activityDuration),
1425
+ completion: true
1426
+ }
1427
+
1428
+ break
1518
1429
  }
1519
- .md-controller {
1520
- margin-left: 0px;
1521
- margin-right: 0px;
1522
- .ctrl-play {
1523
- margin-left: 0px;
1524
- margin-right: 0px;
1430
+
1431
+ case 'LESSON': {
1432
+ //======================================================
1433
+ let completedSize = 0
1434
+ Object.entries(this.getAllCompleted).forEach((value) => {
1435
+ completedSize = completedSize + value[1].length
1436
+ })
1437
+
1438
+ if (completedSize === this.getAllActivities().pageSize) {
1439
+ let text
1440
+ //Defining the text to display for stmt description and definition
1441
+ switch (this.$i18n.locale) {
1442
+ case 'fr':
1443
+ if (this.getModuleInfo.courseID)
1444
+ text = `Le ${this.getModuleInfo.id} de ${this.getModuleInfo.courseID}`
1445
+ else text = `Le ${this.getModuleInfo.id}`
1446
+ break
1447
+ case 'en':
1448
+ if (this.getModuleInfo.courseID)
1449
+ text = `The ${this.getModuleInfo.id} of ${this.getModuleInfo.courseID}`
1450
+ else text = `The ${this.getModuleInfo.id}`
1451
+ break
1452
+ }
1453
+
1454
+ // Retrive only user data in lessons front its interaction that we want to send to the LRS.
1455
+ // Note: User Settings are sent on a different URI and statement
1456
+ const { isFistTime, userSettings, ...lessonsData } =
1457
+ this.getUserInteraction
1458
+
1459
+ stmt = {
1460
+ verb: 'completed',
1461
+ definition: text,
1462
+ description: text,
1463
+ extensions: [
1464
+ {
1465
+ id: 'user-data',
1466
+ content: {
1467
+ routeHistory: this.getRouteHistory,
1468
+ ...lessonsData
1469
+ }
1470
+ }
1471
+ ],
1472
+ duration: this.appTimer.ISOTimeParser(this.lessonDuration),
1473
+ completion: true
1474
+ }
1475
+ this.lessonCompletionStatus = true // set the status of this lesson as completed
1476
+ let completedState = {
1477
+ duration: this.appTimer.ISOTimeParser(this.lessonDuration),
1478
+ completion: this.lessonCompletionStatus
1479
+ }
1480
+ this.setCompletionState(completedState)
1525
1481
  }
1482
+
1483
+ break
1526
1484
  }
1527
1485
  }
1528
- }
1486
+ if (!stmt) return
1529
1487
 
1530
- #primary_nav_wrapper {
1531
- display: flex;
1532
- justify-content: center;
1533
- }
1534
- #footer-dr {
1535
- width: 100%;
1536
- height: $heigthNavbar;
1537
- justify-content: center;
1538
- opacity: 1;
1539
- position: relative;
1540
- transition: all ease-in 0.5s !important;
1541
- &.hidden {
1542
- bottom: (-55px);
1543
- opacity: 0;
1544
- pointer-events: none;
1545
- }
1546
- .app-ctrl {
1547
- width: 100%;
1548
- .col {
1549
- display: flex;
1550
- #btn-previous {
1551
- width: 50%;
1552
- }
1553
- #btn-next {
1554
- width: 50%;
1555
- position: absolute;
1556
- right: 0;
1557
- }
1488
+ this.$bus.$emit('send-xapi-statement', stmt)
1489
+ },
1490
+ /**
1491
+ * @description Method handle start event of activity to LRS
1492
+ * Check if the activity is already initiated in The LRS record to determine wether it a start or a resume
1493
+ * @param {Object} a - Data of the activity
1494
+ * @fires 'send-xapi-statement' to APPBASE
1495
+ */
1496
+
1497
+ sendStartStatement(a) {
1498
+ if (!a) return
1499
+ const { id } = a
1500
+ let aName = null
1501
+
1502
+ switch (true) {
1503
+ case id == 'A00':
1504
+ aName = 'Introduction'
1505
+ break
1506
+ case id == 'A99':
1507
+ aName = 'Conclusion'
1508
+ break
1509
+
1510
+ default: {
1511
+ let d = id.replace('A', '').trim()
1512
+ d = parseInt(d)
1513
+ aName = `${this.$t('text.activity')} ${d}`
1558
1514
  }
1559
1515
  }
1516
+
1517
+ let text =
1518
+ this.$i18n.locale == 'fr'
1519
+ ? `${aName} de ${this.getModuleInfo.id}`
1520
+ : `${aName} of ${this.getModuleInfo.id}`
1521
+
1522
+ /*
1523
+ *Determine if activity as been initialized. Activity is initialized when it is in the serverRecords
1524
+ */
1525
+
1526
+ const thisActivityServerState = this.getUserInteraction[id]
1527
+ ? this.getUserInteraction[id]
1528
+ : {}
1529
+ const recordInServerLength = Object.keys(thisActivityServerState).length
1530
+
1531
+ const stmt = {
1532
+ id,
1533
+ verb: recordInServerLength ? 'resumed' : 'initialized', //determine verb of the statement to send
1534
+ definition: text,
1535
+ description: text
1536
+ }
1537
+ setTimeout(() => this.$bus.$emit('send-xapi-statement', stmt), 500)
1560
1538
  }
1561
1539
  }
1562
1540
  }
1541
+ </script>
1542
+ <style lang="scss">
1543
+ .module {
1544
+ width: 100%;
1545
+ height: 100%;
1546
+ min-height: 100vh;
1547
+ position: relative;
1548
+ display: flex;
1549
+ flex-direction: row;
1550
+ align-items: stretch;
1551
+ }
1563
1552
 
1564
- #btn_mobile_hide {
1565
- width: 48px;
1566
- height: 40px;
1567
- border: none;
1553
+ .skip-link {
1554
+ position: absolute;
1555
+ left: -999px;
1556
+ top: auto;
1557
+ width: 1px;
1558
+ height: 1px;
1559
+ overflow: hidden;
1560
+ z-index: -999;
1561
+
1562
+ &:focus,
1563
+ &:active {
1564
+ left: auto;
1565
+ top: auto;
1566
+ width: 30%;
1567
+ height: auto;
1568
+ overflow: auto;
1569
+ margin: 10px 35%;
1570
+ padding: 5px;
1571
+ text-align: center;
1572
+ font-size: 1.2em;
1573
+ z-index: 999;
1574
+ }
1575
+ }
1576
+ /********** FM **********/
1577
+ #Loading {
1578
+ width: 100%;
1579
+ position: fixed;
1580
+ z-index: 99999;
1581
+ left: 0;
1582
+ opacity: 0;
1568
1583
  display: none;
1584
+ transition: opacity 0.5s;
1569
1585
  pointer-events: none;
1586
+ }
1570
1587
 
1571
- @media screen and (max-width: 1100px) {
1572
- display: block;
1573
- pointer-events: all;
1574
- position: absolute;
1575
- top: 6px;
1576
- right: 10px;
1577
- z-index: 10;
1588
+ #right-sidebar {
1589
+ z-index: 10;
1590
+ display: flex;
1591
+ flex-direction: column;
1592
+ flex-wrap: nowrap;
1593
+ justify-content: flex-start;
1594
+ max-width: 780px;
1595
+ width: 30%;
1596
+ height: 100%;
1597
+ overflow: hidden;
1598
+ position: fixed;
1599
+ right: 0;
1600
+ top: 0;
1601
+ background-color: #ffffff;
1602
+ -webkit-box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1603
+ box-shadow: -2px 1px 6px -1px rgb(0 0 0 / 40%);
1604
+
1605
+ #right-sidebar-header {
1606
+ display: flex;
1607
+ flex-direction: column;
1608
+ align-items: flex-end;
1609
+ padding: 32px 20px 0 32px;
1610
+
1611
+ .embranchement-close {
1612
+ padding: 11px 13px;
1613
+ }
1578
1614
  }
1579
1615
 
1580
- svg {
1581
- transition: transform 0.5s ease-out;
1582
- transform: rotate(180deg);
1583
-
1584
- &[data-hide='true'] {
1585
- transform: rotate(0deg);
1586
- }
1616
+ #right-sidebar-body {
1617
+ max-height: 90%;
1618
+ overflow-y: auto;
1587
1619
  }
1588
1620
  }
1621
+
1622
+ .right-sidebar-transition-enter-active {
1623
+ transition: transform 0.33s ease-out;
1624
+ }
1625
+
1626
+ .right-sidebar-transition-leave-active {
1627
+ transition: transform 0.16s;
1628
+ }
1629
+
1630
+ .right-sidebar-transition-enter-from,
1631
+ .right-sidebar-transition-leave-to {
1632
+ transform: translateX(100%);
1633
+ }
1589
1634
  </style>