fcad-core-dragon 2.0.1 → 2.0.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +33 -33
- package/.eslintignore +29 -29
- package/.eslintrc.cjs +81 -81
- package/CHANGELOG +17 -1
- package/bk.scss +117 -117
- package/package.json +1 -1
- package/src/$locales/en.json +18 -4
- package/src/$locales/fr.json +17 -3
- package/src/assets/data/onboardingMessages.json +47 -47
- package/src/components/AppBase.vue +36 -341
- package/src/components/AppBaseErrorDisplay.vue +438 -438
- package/src/components/AppBaseFlipCard.vue +84 -84
- package/src/components/AppBaseModule.vue +16 -21
- package/src/components/AppBasePage.vue +45 -14
- package/src/components/AppBasePopover.vue +41 -41
- package/src/components/AppBaseSkeleton.vue +45 -0
- package/src/components/AppCompAudio.vue +12 -3
- package/src/components/AppCompButtonProgress.vue +13 -2
- package/src/components/AppCompCarousel.vue +12 -4
- package/src/components/AppCompInputCheckBoxNx.vue +324 -0
- package/src/components/AppCompInputDropdownNx.vue +295 -0
- package/src/components/AppCompInputRadioNx.vue +264 -0
- package/src/components/AppCompInputTextNx.vue +148 -0
- package/src/components/AppCompInputTextTableNx.vue +198 -0
- package/src/components/AppCompInputTextToFillDropdownNx.vue +291 -0
- package/src/components/AppCompInputTextToFillNx.vue +277 -0
- package/src/components/AppCompJauge.vue +11 -4
- package/src/components/AppCompMenu.vue +7 -14
- package/src/components/AppCompMenuItem.vue +7 -5
- package/src/components/AppCompNavigation.vue +21 -21
- package/src/components/AppCompNoteCall.vue +1 -0
- package/src/components/AppCompNoteCredit.vue +2 -1
- package/src/components/AppCompPlayBarNext.vue +94 -41
- package/src/components/AppCompPlayBarProgress.vue +82 -82
- package/src/components/AppCompPopUpNext.vue +6 -6
- package/src/components/AppCompQuiz.vue +500 -0
- package/src/components/AppCompQuizRecall.vue +113 -66
- package/src/components/AppCompSettingsMenu.vue +172 -172
- package/src/components/AppCompTableOfContent.vue +39 -10
- package/src/components/AppCompVideoPlayer.vue +1 -1
- package/src/components/AppCompViewDisplay.vue +6 -6
- package/src/composables/useQuiz.js +62 -179
- package/src/directives/nvdaFix.js +53 -0
- package/src/externalComps/ModuleView.vue +22 -22
- package/src/externalComps/SummaryView.vue +91 -91
- package/src/main.js +227 -30
- package/src/mixins/$mediaMixins.js +1 -11
- package/src/module/stores/appStore.js +29 -11
- package/src/module/xapi/Crypto/Hasher.js +241 -241
- package/src/module/xapi/Crypto/WordArray.js +278 -278
- package/src/module/xapi/Crypto/algorithms/BufferedBlockAlgorithm.js +103 -103
- package/src/module/xapi/Crypto/algorithms/C_algo.js +315 -315
- package/src/module/xapi/Crypto/algorithms/HMAC.js +9 -9
- package/src/module/xapi/Crypto/algorithms/SHA1.js +9 -9
- package/src/module/xapi/Crypto/encoders/Base.js +105 -105
- package/src/module/xapi/Crypto/encoders/Base64.js +99 -99
- package/src/module/xapi/Crypto/encoders/Hex.js +61 -61
- package/src/module/xapi/Crypto/encoders/Latin1.js +61 -61
- package/src/module/xapi/Crypto/encoders/Utf8.js +45 -45
- package/src/module/xapi/Crypto/index.js +53 -53
- package/src/module/xapi/Statement/activity.js +47 -47
- package/src/module/xapi/Statement/agent.js +55 -55
- package/src/module/xapi/Statement/group.js +26 -26
- package/src/module/xapi/Statement/index.js +259 -259
- package/src/module/xapi/Statement/statement.js +253 -253
- package/src/module/xapi/Statement/statementRef.js +23 -23
- package/src/module/xapi/Statement/substatement.js +22 -22
- package/src/module/xapi/Statement/verb.js +36 -36
- package/src/module/xapi/activitytypes.js +17 -17
- package/src/module/xapi/utils.js +167 -167
- package/src/module/xapi/verbs.js +294 -294
- package/src/module/xapi/xapiStatement.js +444 -444
- package/src/plugins/bus.js +8 -8
- package/src/plugins/gsap.js +14 -14
- package/src/plugins/i18n.js +44 -44
- package/src/plugins/idb.js +1 -1
- package/src/plugins/save.js +37 -37
- package/src/plugins/scorm.js +287 -287
- package/src/plugins/xapi.js +11 -11
- package/src/public/index.html +33 -33
- package/src/shared/generalfuncs.js +134 -0
- package/src/shared/validators.js +308 -234
- package/src/components/AppCompInputCheckBoxNext.vue +0 -205
- package/src/components/AppCompInputDropdownNext.vue +0 -201
- package/src/components/AppCompInputRadioNext.vue +0 -158
- package/src/components/AppCompInputTextNext.vue +0 -124
- package/src/components/AppCompInputTextTableNext.vue +0 -142
- package/src/components/AppCompInputTextToFillDropdownNext.vue +0 -238
- package/src/components/AppCompInputTextToFillNext.vue +0 -171
- package/src/components/AppCompQuizNext.vue +0 -2908
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<!-- About this Component--
|
|
2
|
+
* Renders a Series of inputs to collect input for the Quiz component.
|
|
3
|
+
* Related Quiz to question: TEXTE_TROUE_SELECT
|
|
4
|
+
* Receives the a data object defined by user
|
|
5
|
+
* Used by AppCompQuiz
|
|
6
|
+
* Uses useQuiz composable
|
|
7
|
+
-->
|
|
8
|
+
<template>
|
|
9
|
+
<div v-if="inputData.length > 0" :id="id" class="input-box">
|
|
10
|
+
<fieldset :aria-label="fieldsetLabel">
|
|
11
|
+
<div
|
|
12
|
+
v-for="textInput in inputElements"
|
|
13
|
+
:key="textInput.id"
|
|
14
|
+
class="texteatrou"
|
|
15
|
+
>
|
|
16
|
+
<span
|
|
17
|
+
v-if="textInput.type == 'text' && textInput.content.trim() !== ''"
|
|
18
|
+
v-html="textInput.content"
|
|
19
|
+
></span>
|
|
20
|
+
<label :for="`${id}_${textInput.id}-champ`" style="display: none">
|
|
21
|
+
{{ $t('text.quiz') }}
|
|
22
|
+
</label>
|
|
23
|
+
<div
|
|
24
|
+
v-if="textInput.type == 'select'"
|
|
25
|
+
class="cnt-input"
|
|
26
|
+
:class="`${retro[textInput.id.substr(textInput.id.length - 1)] ? retro[textInput.id.substr(textInput.id.length - 1)] : ''}`"
|
|
27
|
+
>
|
|
28
|
+
<v-select
|
|
29
|
+
:id="`${id}_${textInput.id}-champ`"
|
|
30
|
+
:model-value="inputsValue[textInput.index]"
|
|
31
|
+
item-title="text"
|
|
32
|
+
:open-text="`${$t('message.dropdown_list')}`"
|
|
33
|
+
:item-props="true"
|
|
34
|
+
:items="getItemOptions(textInput.index)"
|
|
35
|
+
:aria-describedby="`${id}_${textInput.id}-msg-erreur`"
|
|
36
|
+
:aria-labelledby="`${id}_${textInput.id}-label`"
|
|
37
|
+
@update:model-value="onSelectUpdate($event, textInput.index)"
|
|
38
|
+
/>
|
|
39
|
+
<span
|
|
40
|
+
:id="`${id}_${textInput.id}-msg-erreur`"
|
|
41
|
+
:key="`msg_chx_${id}_${textInput.id}`"
|
|
42
|
+
class="sr-only"
|
|
43
|
+
>
|
|
44
|
+
{{ messageAccessibility[textInput.index] }}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</fieldset>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
<script>
|
|
52
|
+
import { useQuiz } from '../composables/useQuiz'
|
|
53
|
+
export default {
|
|
54
|
+
name: 'AppCompInputTextToFillDropdownNext',
|
|
55
|
+
/* PROPS USED FOR PARENT TO COMMUNICATE DATA TO CHILD */
|
|
56
|
+
props: {
|
|
57
|
+
modelValue: {
|
|
58
|
+
type: Array,
|
|
59
|
+
default: () => []
|
|
60
|
+
},
|
|
61
|
+
inputData: {
|
|
62
|
+
type: Array,
|
|
63
|
+
default: () => []
|
|
64
|
+
},
|
|
65
|
+
textBase: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: ''
|
|
68
|
+
},
|
|
69
|
+
solution: {
|
|
70
|
+
type: Array,
|
|
71
|
+
default: () => []
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
id: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: ''
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
emits: ['update:modelValue', 'enable-submit'],
|
|
80
|
+
setup(props) {
|
|
81
|
+
const { retroType, addRetroStyle, resetRetroStyle } = useQuiz()
|
|
82
|
+
|
|
83
|
+
return { retroType, addRetroStyle, resetRetroStyle }
|
|
84
|
+
},
|
|
85
|
+
data() {
|
|
86
|
+
return {
|
|
87
|
+
inputElements: [],
|
|
88
|
+
result: [],
|
|
89
|
+
retro: [],
|
|
90
|
+
messageAccessibility: [],
|
|
91
|
+
inputCount: null
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
computed: {
|
|
95
|
+
fieldsetLabel() {
|
|
96
|
+
return `${this.$t('quizState.textToFillDropdownFieldset1')} ${this.inputCount} ${this.$t('quizState.textToFillDropdownFieldset2')}`
|
|
97
|
+
},
|
|
98
|
+
/* INTERNAL REACTIVE VALUE THAT BIND WITH MODELVALUE PROPS */
|
|
99
|
+
inputsValue: {
|
|
100
|
+
get() {
|
|
101
|
+
return this.modelValue
|
|
102
|
+
},
|
|
103
|
+
set(newValue) {
|
|
104
|
+
this.resetRetroStyle([this.retro, this.messageAccessibility])
|
|
105
|
+
this.$emit('update:modelValue', [...newValue])
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
/**
|
|
109
|
+
* internal reactive value that map the options to add SELECTED attribute on each option
|
|
110
|
+
* and Add entrie for the default selected Elements
|
|
111
|
+
* this attribute value can be changed when user select the option
|
|
112
|
+
*/
|
|
113
|
+
mappedOptions() {
|
|
114
|
+
return this.inputData.map((op) => {
|
|
115
|
+
const key = Object.keys(op)[0]
|
|
116
|
+
const values = Object.values(op)[0]
|
|
117
|
+
|
|
118
|
+
const newValues = [
|
|
119
|
+
{
|
|
120
|
+
value: null,
|
|
121
|
+
disabled: true,
|
|
122
|
+
selected: true,
|
|
123
|
+
text: this.$t('message.first_option_dropdown')
|
|
124
|
+
},
|
|
125
|
+
...values.map((el) => ({
|
|
126
|
+
value: el,
|
|
127
|
+
text: el,
|
|
128
|
+
selected: false
|
|
129
|
+
}))
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
[key]: newValues
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
watch: {
|
|
139
|
+
/**
|
|
140
|
+
* @description watcher to track the inputs content
|
|
141
|
+
* remove an element from the inputs array (modelValue) when it is empty
|
|
142
|
+
*/
|
|
143
|
+
inputsValue: {
|
|
144
|
+
handler() {
|
|
145
|
+
const filledCount = this.modelValue.filter(
|
|
146
|
+
(val) => val !== null && val !== undefined
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
this.$emit('enable-submit', filledCount.length == this.inputData.length)
|
|
150
|
+
},
|
|
151
|
+
deep: true,
|
|
152
|
+
immediate: true
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
mounted() {},
|
|
157
|
+
created() {
|
|
158
|
+
this.createTextWithInput(this.textBase)
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
methods: {
|
|
162
|
+
/**
|
|
163
|
+
* @description create the object to generate the text and selects options
|
|
164
|
+
* for each fillable input. The created object is assigned to inputElements
|
|
165
|
+
* @param {String} str the text with holes to fill
|
|
166
|
+
*/
|
|
167
|
+
createTextWithInput(str) {
|
|
168
|
+
const regex = /\$%\S*%\$/g // regex pattern to match exp: $%number%$
|
|
169
|
+
|
|
170
|
+
let splittedText = str.split(regex)
|
|
171
|
+
let listInput = []
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < splittedText.length - 1; i++) {
|
|
174
|
+
listInput.push({
|
|
175
|
+
id: `text-${i}`,
|
|
176
|
+
type: 'text',
|
|
177
|
+
content: splittedText[i]
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (i < splittedText.length - 1)
|
|
181
|
+
listInput.push({ id: `select-${i}`, type: 'select', index: i })
|
|
182
|
+
}
|
|
183
|
+
let lI = listInput.findLast((element) => element.type == 'select')
|
|
184
|
+
this.inputCount = lI.index + 1
|
|
185
|
+
|
|
186
|
+
this.inputElements = listInput
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* @description Method to force update of this.inputsValue.
|
|
190
|
+
* this force the old value and new value of the input to be tracked by VUE.
|
|
191
|
+
* @param newValue, the new value of the input
|
|
192
|
+
* @param index index of the selected element
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
onSelectUpdate(newValue, index) {
|
|
196
|
+
if (this.$el && this.$el.id !== this.id) return //prevent event from firing on other input
|
|
197
|
+
this.resetRetroStyle([this.retro, this.messageAccessibility])
|
|
198
|
+
const updated = [...this.inputsValue] //create new array from inputsValue
|
|
199
|
+
updated[index] = newValue
|
|
200
|
+
this.inputsValue = updated //trigger vue reactivity for inputsValue
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @description Method to retrive the options of each selectable input in the mappedOption Object
|
|
205
|
+
* @param {Number } index - number representing the index of the item in the array of inputElements
|
|
206
|
+
* @Return a collection of item representing the options for a the selectables
|
|
207
|
+
*/
|
|
208
|
+
getItemOptions(index) {
|
|
209
|
+
return Object.values(this.mappedOptions[index])[0]
|
|
210
|
+
},
|
|
211
|
+
/**
|
|
212
|
+
* @description Component validation method to validate the user input against the solution
|
|
213
|
+
* can handle logic of Css style to apply (correct/wrong Answer) and return the result
|
|
214
|
+
* @return {Object} result
|
|
215
|
+
*/
|
|
216
|
+
validateAnswer() {
|
|
217
|
+
let mappedResults
|
|
218
|
+
|
|
219
|
+
if (this.solution != null) {
|
|
220
|
+
mappedResults = this.inputsValue.map((a, i) => {
|
|
221
|
+
const s = Object.values(this.solution[i])[0]
|
|
222
|
+
return { selected: a, correct: s == a }
|
|
223
|
+
})
|
|
224
|
+
} else {
|
|
225
|
+
mappedResults = this.inputsValue.map((a) => {
|
|
226
|
+
return { selected: a, correct: false }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
//Set the valition style
|
|
231
|
+
let { classRetro, mesA11y } = this.addRetroStyle(
|
|
232
|
+
this.solution,
|
|
233
|
+
mappedResults,
|
|
234
|
+
this.inputsValue.length
|
|
235
|
+
)
|
|
236
|
+
this.retro = classRetro
|
|
237
|
+
this.messageAccessibility = mesA11y
|
|
238
|
+
//Retrive the user response from validation
|
|
239
|
+
this.result = mappedResults.filter((a) => a.selected)
|
|
240
|
+
|
|
241
|
+
let retro = this.retroType(this.solution, this.result)
|
|
242
|
+
|
|
243
|
+
let cAns
|
|
244
|
+
if (this.solution != null) cAns = this.computeResult(this.result)
|
|
245
|
+
else cAns = false
|
|
246
|
+
//Return this to parent
|
|
247
|
+
return {
|
|
248
|
+
userAnswer: this.result,
|
|
249
|
+
correctAnswer: cAns,
|
|
250
|
+
retroType: retro
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @description - compute the correct elements in user response
|
|
256
|
+
* compare with the solution and return wether user succeeded or failed
|
|
257
|
+
* @param {Object} result - user responses
|
|
258
|
+
* @retuns boolean expressing the fail/success result
|
|
259
|
+
*/
|
|
260
|
+
computeResult(result) {
|
|
261
|
+
const res = result.filter((el) => el.correct)
|
|
262
|
+
return res.length == this.solution.length
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
267
|
+
<style lang="scss" scoped>
|
|
268
|
+
select {
|
|
269
|
+
&.custom-select {
|
|
270
|
+
background-image: inherit;
|
|
271
|
+
|
|
272
|
+
&:focus {
|
|
273
|
+
border-color: inherit;
|
|
274
|
+
box-shadow: inherit;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/***** style dev *****/
|
|
280
|
+
.texteatrou {
|
|
281
|
+
display: inline;
|
|
282
|
+
}
|
|
283
|
+
.texteatrou > select {
|
|
284
|
+
display: inline;
|
|
285
|
+
width: auto;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cnt-input {
|
|
289
|
+
position: relative;
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
<!-- About this Component--
|
|
2
|
+
* Renders a Series of inputs to collect input for the Quiz component.
|
|
3
|
+
* Related Quiz to question: TEXTE_TROUE
|
|
4
|
+
* Receives the a data object defined by user
|
|
5
|
+
* Used by AppCompQuiz
|
|
6
|
+
* Uses useQuiz composable
|
|
7
|
+
-->
|
|
8
|
+
<template>
|
|
9
|
+
<div v-if="inputElements.length > 0" :id="id" class="input-box">
|
|
10
|
+
<fieldset :aria-label="fieldsetLabel">
|
|
11
|
+
<div
|
|
12
|
+
v-for="textInput in inputElements"
|
|
13
|
+
:key="textInput.id"
|
|
14
|
+
class="texteatrou"
|
|
15
|
+
>
|
|
16
|
+
<span
|
|
17
|
+
v-if="textInput.type == 'text' && textInput.content.trim() !== ''"
|
|
18
|
+
v-html="textInput.content"
|
|
19
|
+
></span>
|
|
20
|
+
<label :for="`${id}_${textInput.id}-champ`" style="display: none">
|
|
21
|
+
{{ $t('text.quiz') }}
|
|
22
|
+
</label>
|
|
23
|
+
|
|
24
|
+
<v-text-field
|
|
25
|
+
v-if="textInput.type == 'input'"
|
|
26
|
+
:id="`${id}_${textInput.id}-champ`"
|
|
27
|
+
:model-value="inputsValue[textInput.index]"
|
|
28
|
+
:placeholder="$t('text.place_holder.for_textarea')"
|
|
29
|
+
no-resize
|
|
30
|
+
:class="`${retro[textInput.id.substr(textInput.id.length - 1)]}`"
|
|
31
|
+
class="input-textatrou"
|
|
32
|
+
:aria-describedby="`${id}_${textInput.id}-msg-erreur`"
|
|
33
|
+
:aria-labelledby="`${id}_${textInput.id}-label`"
|
|
34
|
+
@update:model-value="onSelectUpdate($event, textInput.index)"
|
|
35
|
+
/>
|
|
36
|
+
<span
|
|
37
|
+
:id="`${id}_${textInput.id}-msg-erreur`"
|
|
38
|
+
:key="`msg_chx_${id}_${textInput.id}`"
|
|
39
|
+
class="sr-only"
|
|
40
|
+
>
|
|
41
|
+
{{ messageAccessibility[textInput.index] }}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
</fieldset>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
<script>
|
|
48
|
+
import { useQuiz } from '../composables/useQuiz'
|
|
49
|
+
import { validateObjType } from '../shared/validators'
|
|
50
|
+
export default {
|
|
51
|
+
name: 'AppCompInputTextToFillNext',
|
|
52
|
+
/* PROPS USED FOR PARENT TO COMMUNICATE DATA TO CHILD */
|
|
53
|
+
props: {
|
|
54
|
+
modelValue: {
|
|
55
|
+
type: Array,
|
|
56
|
+
default: () => []
|
|
57
|
+
},
|
|
58
|
+
inputData: {
|
|
59
|
+
type: Array,
|
|
60
|
+
default: () => []
|
|
61
|
+
},
|
|
62
|
+
textBase: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: ''
|
|
65
|
+
},
|
|
66
|
+
solution: {
|
|
67
|
+
type: Array,
|
|
68
|
+
default: () => []
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
id: {
|
|
72
|
+
type: String,
|
|
73
|
+
default: ''
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
emits: ['update:modelValue', 'enable-submit'],
|
|
77
|
+
setup(props) {
|
|
78
|
+
const { retroType, addRetroStyle, resetRetroStyle } = useQuiz()
|
|
79
|
+
|
|
80
|
+
return { retroType, addRetroStyle, resetRetroStyle }
|
|
81
|
+
},
|
|
82
|
+
data() {
|
|
83
|
+
return {
|
|
84
|
+
quizInputDataValue: [],
|
|
85
|
+
textInputs: [],
|
|
86
|
+
quizSolution: null,
|
|
87
|
+
inputElements: [],
|
|
88
|
+
result: null,
|
|
89
|
+
retro: [],
|
|
90
|
+
messageAccessibility: [],
|
|
91
|
+
inputCount: null
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
computed: {
|
|
95
|
+
fieldsetLabel() {
|
|
96
|
+
return `${this.$t('quizState.textToFillFieldset1')} ${this.inputCount} ${this.$t('quizState.textToFillFieldset2')}`
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/* INTERNAL REACTIVE VALUE THAT BIND WITH MODELVALUE PROPS */
|
|
100
|
+
inputsValue: {
|
|
101
|
+
get() {
|
|
102
|
+
return this.modelValue
|
|
103
|
+
},
|
|
104
|
+
set(newValue) {
|
|
105
|
+
this.$emit('update:modelValue', [...newValue])
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
watch: {
|
|
111
|
+
/**
|
|
112
|
+
* @description watcher to track the inputs content
|
|
113
|
+
* remove an element from the inputs array (modelValue) when it is empty
|
|
114
|
+
*/
|
|
115
|
+
inputsValue: {
|
|
116
|
+
async handler() {
|
|
117
|
+
const filled = this.modelValue.filter((val) => val && val.trim() !== '')
|
|
118
|
+
if (this.inputCount == null)
|
|
119
|
+
this.inputCount = await this.getInputElements()
|
|
120
|
+
this.$emit('enable-submit', filled.length == this.inputCount)
|
|
121
|
+
},
|
|
122
|
+
deep: true,
|
|
123
|
+
immediate: true
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
created() {
|
|
127
|
+
this.createTextWithInput(this.textBase)
|
|
128
|
+
},
|
|
129
|
+
mounted() {},
|
|
130
|
+
|
|
131
|
+
methods: {
|
|
132
|
+
/**
|
|
133
|
+
* @description validate the raw data received by the component to render is view
|
|
134
|
+
* @returns {Object} errors - errorList: to display in view and errorConsole, to be displayed in console
|
|
135
|
+
*/
|
|
136
|
+
validateInputData() {
|
|
137
|
+
let errors = null //array for errors dectected
|
|
138
|
+
let stringType = ['id', 'value']
|
|
139
|
+
|
|
140
|
+
if (!this.inputData.length) return errors
|
|
141
|
+
for (let i = 0; i < this.inputData.length; i++) {
|
|
142
|
+
errors = validateObjType(
|
|
143
|
+
this.inputData[i],
|
|
144
|
+
{ stringType },
|
|
145
|
+
null,
|
|
146
|
+
`choix_reponse #${i + 1}`
|
|
147
|
+
)
|
|
148
|
+
const { errorList, errorInConsole } = errors
|
|
149
|
+
|
|
150
|
+
if (errorList.length || errorInConsole.length) return errors
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return errors
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* @description create the object to genate the text and inputs
|
|
157
|
+
* @param {String} str the text with holes to fill
|
|
158
|
+
*/
|
|
159
|
+
createTextWithInput(str) {
|
|
160
|
+
const regex = /\$%\S*%\$/g // regex pattern to match exp: $%number%$
|
|
161
|
+
|
|
162
|
+
let splittedText = str.split(regex)
|
|
163
|
+
let listInput = []
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < splittedText.length - 1; i++) {
|
|
166
|
+
listInput.push({
|
|
167
|
+
id: `text-${i}`,
|
|
168
|
+
type: 'text',
|
|
169
|
+
content: splittedText[i]
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
if (i < splittedText.length - 1)
|
|
173
|
+
listInput.push({ id: `input-${i}`, type: 'input', index: i })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.inputElements = listInput
|
|
177
|
+
},
|
|
178
|
+
/**
|
|
179
|
+
* @description Method to force update of this.inputsValue.
|
|
180
|
+
* this force the old value and new value of the input to be tracked by VUE.
|
|
181
|
+
* @param newValue, the new value of the input
|
|
182
|
+
* @param index index of the selected element
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
onSelectUpdate(newValue, index) {
|
|
186
|
+
if (this.$el && this.$el.id !== this.id) return
|
|
187
|
+
this.resetRetroStyle([this.retro, this.messageAccessibility])
|
|
188
|
+
|
|
189
|
+
const updated = [...this.inputsValue] //create new array from inputsValue
|
|
190
|
+
updated[index] = newValue
|
|
191
|
+
this.inputsValue = updated //trigger vue reactivity for inputsValue
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @description Component validation method to validate the user input against the solution
|
|
196
|
+
* can handle logic of Css style to apply (correct/wrong Answer) and return the result
|
|
197
|
+
* @return {Object} result
|
|
198
|
+
*/
|
|
199
|
+
validateAnswer() {
|
|
200
|
+
let mappedResults
|
|
201
|
+
if (this.solution != null) {
|
|
202
|
+
mappedResults = this.inputsValue.map((a, i) => {
|
|
203
|
+
a = a.trim().toLowerCase()
|
|
204
|
+
const s = Object.values(this.solution[i])[0]
|
|
205
|
+
return { filled: a, correct: s.includes(a) }
|
|
206
|
+
})
|
|
207
|
+
} else {
|
|
208
|
+
mappedResults = this.inputsValue.map((a, i) => {
|
|
209
|
+
return { filled: a, correct: false }
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//========================================================
|
|
214
|
+
//Set the valition style
|
|
215
|
+
let { classRetro, mesA11y } = this.addRetroStyle(
|
|
216
|
+
this.solution,
|
|
217
|
+
mappedResults,
|
|
218
|
+
this.inputsValue.length
|
|
219
|
+
)
|
|
220
|
+
this.retro = classRetro
|
|
221
|
+
this.messageAccessibility = mesA11y
|
|
222
|
+
//Retrive the user response from validation
|
|
223
|
+
this.result = mappedResults.filter((a) => a.filled)
|
|
224
|
+
|
|
225
|
+
let retro = this.retroType(this.solution, this.result)
|
|
226
|
+
|
|
227
|
+
let cAns
|
|
228
|
+
if (this.solution != null) cAns = this.computeResult(this.result)
|
|
229
|
+
else cAns = false
|
|
230
|
+
//========================================================
|
|
231
|
+
//Return this to parent
|
|
232
|
+
return {
|
|
233
|
+
userAnswer: this.result,
|
|
234
|
+
correctAnswer: cAns,
|
|
235
|
+
retroType: retro
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @description - compute the correct elements in user response
|
|
241
|
+
* compare with the solution and return wether user succeeded or failed
|
|
242
|
+
* @param {Object} result - user responses
|
|
243
|
+
* @retuns boolean
|
|
244
|
+
*/
|
|
245
|
+
computeResult(result) {
|
|
246
|
+
const res = this.result.filter((el) => el.correct)
|
|
247
|
+
return res.length == this.solution.length
|
|
248
|
+
},
|
|
249
|
+
/**
|
|
250
|
+
* @description - Method to ge the Number of input elements in the component
|
|
251
|
+
* This is used to determine if the user has filled all inputs before enabling the submit button
|
|
252
|
+
* @returns {Promise} resolves to the number of input elements
|
|
253
|
+
*/
|
|
254
|
+
async getInputElements() {
|
|
255
|
+
await this.$nextTick()
|
|
256
|
+
// Ensure this is the targeted element
|
|
257
|
+
if (this.$el.id !== this.id) return
|
|
258
|
+
return this.inputElements.filter((el) => el.type == 'input').length
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
</script>
|
|
263
|
+
<style lang="scss" scoped>
|
|
264
|
+
.texteatrou {
|
|
265
|
+
display: inline;
|
|
266
|
+
position: relative;
|
|
267
|
+
|
|
268
|
+
.v-input {
|
|
269
|
+
display: inline-block;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.texteatrou > input {
|
|
274
|
+
display: inline;
|
|
275
|
+
width: auto;
|
|
276
|
+
}
|
|
277
|
+
</style>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
:max="maxValue"
|
|
8
8
|
aria-hidden="true"
|
|
9
9
|
></v-progress-linear>
|
|
10
|
-
<p class="prcnt">{{ getPourcent }}
|
|
10
|
+
<p class="prcnt">{{ getPourcent }}</p>
|
|
11
11
|
</div>
|
|
12
12
|
<div v-else>
|
|
13
13
|
<div class="warning">
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
</div>
|
|
21
21
|
</template>
|
|
22
22
|
<script>
|
|
23
|
+
import { mapState } from 'pinia'
|
|
24
|
+
import { useAppStore } from '../module/stores/appStore'
|
|
23
25
|
export default {
|
|
24
26
|
props: {
|
|
25
27
|
// props Give value to show progress
|
|
@@ -39,11 +41,16 @@ export default {
|
|
|
39
41
|
return {}
|
|
40
42
|
},
|
|
41
43
|
computed: {
|
|
44
|
+
...mapState(useAppStore, ['getAppConfigs']),
|
|
42
45
|
getPourcent() {
|
|
43
46
|
// Calculate on a 100%
|
|
44
|
-
let result = (this.value * 100) / this.maxValue
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
let result = Math.round((this.value * 100) / this.maxValue)
|
|
48
|
+
|
|
49
|
+
if (result > 100) result = 100
|
|
50
|
+
|
|
51
|
+
return this.getAppConfigs.lang.toLowerCase() == 'en'
|
|
52
|
+
? `${result}%`
|
|
53
|
+
: `${result} %`
|
|
47
54
|
},
|
|
48
55
|
error() {
|
|
49
56
|
if (typeof this.value != 'number' || typeof this.maxValue != 'number') {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
<app-base-button
|
|
57
57
|
:id="`btn-quiz`"
|
|
58
58
|
class="btn lk-btn"
|
|
59
|
+
title="Réinitialiser la leçon"
|
|
59
60
|
:is-disabled="resetStatus !== false"
|
|
60
61
|
@click="askResetUserData"
|
|
61
62
|
>
|
|
@@ -105,8 +106,8 @@ export default {
|
|
|
105
106
|
'getModuleInfo',
|
|
106
107
|
'getMenuSettings',
|
|
107
108
|
'getAppDebugMode',
|
|
108
|
-
'
|
|
109
|
-
'
|
|
109
|
+
'getAppConfigs',
|
|
110
|
+
'getAllCompleted'
|
|
110
111
|
]),
|
|
111
112
|
appDebugMode() {
|
|
112
113
|
return this.getAppDebugMode
|
|
@@ -171,17 +172,7 @@ export default {
|
|
|
171
172
|
}
|
|
172
173
|
}, 2000)
|
|
173
174
|
},
|
|
174
|
-
//
|
|
175
|
-
// let lastRoute
|
|
176
|
-
// //Get all activity state (menu not included)
|
|
177
|
-
|
|
178
|
-
// //No route history Should link to introduction route
|
|
179
|
-
// if (!this.getRouteHistory.length) return 'introduction'
|
|
180
|
-
// //last Route should be the last element in route history
|
|
181
|
-
// lastRoute = this.getRouteHistory.toReversed()[0]
|
|
182
|
-
// //create the route link and return it
|
|
183
|
-
// return this.createdRoute(lastRoute)
|
|
184
|
-
// },
|
|
175
|
+
//Go to the last route you were before the menu
|
|
185
176
|
GoToLastRoute() {
|
|
186
177
|
let lastRoute
|
|
187
178
|
let path
|
|
@@ -294,7 +285,9 @@ export default {
|
|
|
294
285
|
// Create pourcentage complete
|
|
295
286
|
progress = Math.round((pageCmplt * 100) / page)
|
|
296
287
|
|
|
297
|
-
return
|
|
288
|
+
return this.getAppConfigs.lang.toLowerCase() == 'en'
|
|
289
|
+
? `${progress}%`
|
|
290
|
+
: `${progress} %`
|
|
298
291
|
},
|
|
299
292
|
askResetUserData() {
|
|
300
293
|
//show loader animation
|
|
@@ -35,12 +35,14 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<div class="cnt-time">
|
|
38
|
-
<svg
|
|
38
|
+
<svg
|
|
39
|
+
:aria-label="$t('label.timer')"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
focusable="false"
|
|
42
|
+
>
|
|
39
43
|
<use href="#clock-icon" />
|
|
40
44
|
</svg>
|
|
41
|
-
<p class="time">
|
|
42
|
-
{{ activity.time || '00:00' }}
|
|
43
|
-
</p>
|
|
45
|
+
<p class="time">{{ activity.time || '00:00' }}</p>
|
|
44
46
|
</div>
|
|
45
47
|
<div class="box-gauge">
|
|
46
48
|
<app-comp-jauge
|
|
@@ -76,7 +78,7 @@ export default {
|
|
|
76
78
|
* Apply different punctuation for colons. No space before a colon in english, one space before a colon in french
|
|
77
79
|
*/
|
|
78
80
|
cardTitleSeparator() {
|
|
79
|
-
return this.getAppConfigs.lang
|
|
81
|
+
return this.getAppConfigs.lang.toLowerCase() == 'en' ? ': ' : ' : '
|
|
80
82
|
},
|
|
81
83
|
activities() {
|
|
82
84
|
// get the data for list of the page for this module from the store and
|