@windward/games 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/components/content/blocks/dragDrop/BucketGame.vue +4 -4
  2. package/components/content/blocks/dragDrop/SortingGame.vue +1 -1
  3. package/components/content/blocks/flashcards/FlashcardSlides.vue +2 -2
  4. package/components/content/blocks/matchingGame/MatchingGame.vue +2 -2
  5. package/components/content/blocks/multipleChoice/MultipleChoice.vue +565 -0
  6. package/components/content/blocks/multipleChoice/QuestionDialog.vue +276 -0
  7. package/components/content/blocks/quizshowGame/QuizShow.vue +2 -2
  8. package/components/content/blocks/slideshow/SlideShow.vue +2 -2
  9. package/components/content/blocks/wordJumble/Jumble.vue +97 -0
  10. package/components/content/blocks/wordJumble/WordJumble.vue +239 -0
  11. package/components/settings/MultipleChoiceSettingsManager.vue +290 -0
  12. package/components/settings/SortingGameSettingsManager.vue +10 -5
  13. package/components/settings/WordJumbleSettingsManager.vue +293 -0
  14. package/i18n/en-US/components/content/blocks/index.ts +4 -0
  15. package/i18n/en-US/components/content/blocks/matching_game.ts +1 -1
  16. package/i18n/en-US/components/content/blocks/multiple_choice.ts +11 -0
  17. package/i18n/en-US/components/content/blocks/word_jumble.ts +9 -0
  18. package/i18n/en-US/components/settings/index.ts +6 -0
  19. package/i18n/en-US/components/settings/multiple_choice.ts +16 -0
  20. package/i18n/en-US/components/settings/sorting_game.ts +3 -0
  21. package/i18n/en-US/components/settings/word_jumble.ts +11 -0
  22. package/i18n/en-US/shared/content_blocks.ts +2 -0
  23. package/i18n/en-US/shared/settings.ts +2 -0
  24. package/package.json +1 -1
  25. package/plugin.js +42 -0
  26. package/test/blocks/multipleChoice/MultipleChoice.spec.js +26 -0
  27. package/test/blocks/multipleChoice/QuestionDialog.spec.js +26 -0
  28. package/test/blocks/wordJumble/Jumble.spec.js +27 -0
  29. package/test/blocks/wordJumble/WordJumble.spec.js +24 -0
  30. package/test/settings/MultipleChoiceGameManager.spec.js +30 -0
  31. package/test/settings/WordJumbleManager.spec.js +87 -0
@@ -1,20 +1,20 @@
1
1
  <template>
2
2
  <div>
3
3
  <div>
4
- <h1 class="title" aria-label="bucket game title" tabindex="0">
4
+ <h3 class="title" aria-label="bucket game title" tabindex="0">
5
5
  {{ block.metadata.config.title }}
6
- </h1>
6
+ </h3>
7
7
 
8
8
  <p tabindex="0">
9
9
  {{ block.metadata.config.instructions }}
10
10
  </p>
11
- <h3 v-if="!render" class="d-flex justify-center align-center">
11
+ <h4 v-if="!render" class="d-flex justify-center align-center">
12
12
  {{
13
13
  $t(
14
14
  'plugin.games.components.content.blocks.bucket_game.cannot'
15
15
  )
16
16
  }}
17
- </h3>
17
+ </h4>
18
18
  <v-container fluid :class="status" class="pa-0">
19
19
  <br />
20
20
  <v-row>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div>
3
- <h2>{{ block.metadata.config.title }}</h2>
3
+ <h3>{{ block.metadata.config.title }}</h3>
4
4
 
5
5
  <p>
6
6
  {{ block.metadata.config.instructions }}
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div>
3
- <h2 v-if="block.metadata.config.title">
3
+ <h3 v-if="block.metadata.config.title">
4
4
  {{ block.metadata.config.title }}
5
- </h2>
5
+ </h3>
6
6
 
7
7
  <p v-if="block.metadata.config.instructions">
8
8
  {{ block.metadata.config.instructions }}
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="background">
3
3
  <div>
4
- <h1
4
+ <h3
5
5
  :aria-label="
6
6
  $t(
7
7
  'plugin.games.components.content.blocks.matching_game.match_game_title'
@@ -10,7 +10,7 @@
10
10
  tabindex="0"
11
11
  >
12
12
  {{ block.metadata.config.title }}
13
- </h1>
13
+ </h3>
14
14
 
15
15
  <p tabindex="0">
16
16
  {{ block.metadata.config.instructions }}
@@ -0,0 +1,565 @@
1
+ <template>
2
+ <v-container>
3
+ <v-row>
4
+ <v-col>
5
+ <h3>{{ block.metadata.config.title }}</h3>
6
+ <p>{{ block.metadata.config.instructions }}</p>
7
+ </v-col>
8
+ </v-row>
9
+ <v-row>
10
+ <v-col class="pa-0">
11
+ <v-carousel height="600">
12
+ <v-container>
13
+ <v-carousel-item
14
+ v-for="(question, index) in block.metadata.config
15
+ .questions"
16
+ :key="index"
17
+ >
18
+ <v-col class="d-flex justify-center">
19
+ <p class="questionBody">{{ question.body }}</p>
20
+ </v-col>
21
+ <v-container :key="updateKey">
22
+ <v-row
23
+ v-for="(
24
+ answer, answerIndex
25
+ ) in question.answer_options"
26
+ :key="answerIndex"
27
+ class="mb-2"
28
+ >
29
+ <v-col
30
+ cols="12"
31
+ md="2"
32
+ class="d-flex justify-end"
33
+ >
34
+ <v-icon
35
+ class="isCorrect"
36
+ v-if="
37
+ studentResponses[index] &&
38
+ answer.correctAnswer === true &&
39
+ studentResponses[index]
40
+ .answer_options[answerIndex]
41
+ .chosen === true
42
+ "
43
+ >mdi-check-circle</v-icon
44
+ >
45
+ <v-icon
46
+ class="isCorrect"
47
+ v-if="
48
+ studentResponses[index] &&
49
+ answer.correctAnswer === true &&
50
+ studentResponses[index]
51
+ .answer_options[answerIndex]
52
+ .chosen !== true
53
+ "
54
+ >mdi-check-circle-outline</v-icon
55
+ >
56
+ <v-icon
57
+ class="isIncorrect"
58
+ v-if="
59
+ studentResponses[index] &&
60
+ answer.correctAnswer !== true &&
61
+ studentResponses[index]
62
+ .answer_options[answerIndex]
63
+ .chosen === true
64
+ "
65
+ >mdi-alpha-x-circle</v-icon
66
+ >
67
+ </v-col>
68
+ <v-col cols="12" md="8" class="pa-0">
69
+ <v-card
70
+ class="optionOutline pa-2"
71
+ :disabled="answer.disabled"
72
+ :class="
73
+ onIsThisCorrect(answer, index)
74
+ "
75
+ outlined
76
+ tile
77
+ @click="
78
+ onChooseAnswer(answer, index)
79
+ "
80
+ >
81
+ <div
82
+ class="d-flex justify-space-between"
83
+ >
84
+ <p class="mb-0">
85
+ {{
86
+ getCharacter(
87
+ question,
88
+ answer
89
+ ) +
90
+ ': ' +
91
+ answer.value
92
+ }}
93
+ </p>
94
+ <div>
95
+ <v-icon
96
+ class="isCorrect"
97
+ v-if="
98
+ studentResponses[
99
+ index
100
+ ] &&
101
+ answer.correctAnswer ===
102
+ true &&
103
+ studentResponses[
104
+ index
105
+ ].answer_options[
106
+ answerIndex
107
+ ].chosen !== true
108
+ "
109
+ @click="
110
+ onAnswerDescription(
111
+ question
112
+ )
113
+ "
114
+ >mdi-information</v-icon
115
+ >
116
+ <v-icon
117
+ right
118
+ dark
119
+ nontranslate
120
+ v-if="
121
+ studentResponses[
122
+ index
123
+ ] &&
124
+ answer.correctAnswer ==
125
+ true &&
126
+ studentResponses[
127
+ index
128
+ ].answer_options[
129
+ answerIndex
130
+ ].chosen === true
131
+ "
132
+ @click="
133
+ onAnswerDescription(
134
+ question
135
+ )
136
+ "
137
+ >
138
+ mdi-information-outline
139
+ </v-icon>
140
+ </div>
141
+ </div>
142
+ </v-card>
143
+ </v-col>
144
+ <v-col cols="12" md="2"></v-col>
145
+ </v-row>
146
+ </v-container>
147
+ <v-col cols="12" md="12">
148
+ <v-row class="d-flex justify-center">
149
+ <v-btn
150
+ color="primary"
151
+ outlined
152
+ class="mr-4 hintButton"
153
+ @click="onHint(question)"
154
+ >{{
155
+ $t(
156
+ 'plugin.games.components.content.blocks.multiple_choice.hint'
157
+ )
158
+ }}</v-btn
159
+ >
160
+ <v-btn
161
+ color="primary fiftyButton"
162
+ outlined
163
+ :disabled="
164
+ question.answer_options.length !==
165
+ 4 || question.fiftyFifty
166
+ "
167
+ class="ml-4 fiftyButton"
168
+ @click="onFifty(question)"
169
+ >{{
170
+ $t(
171
+ 'plugin.games.components.content.blocks.multiple_choice.fifty'
172
+ )
173
+ }}</v-btn
174
+ >
175
+ </v-row>
176
+ </v-col>
177
+ <v-layout class="mt-2">
178
+ <v-flex xs4></v-flex>
179
+ <v-flex xs4>
180
+ <v-col align="center" tabindex="0">
181
+ {{
182
+ $t(
183
+ 'plugin.games.components.content.blocks.matching_game.of_complete_text_area',
184
+ [
185
+ completedAmount,
186
+ totalAmountQuestions,
187
+ ]
188
+ )
189
+ }}
190
+ <v-progress-linear
191
+ color="primary"
192
+ outlined
193
+ v-model="completedPercent"
194
+ rounded
195
+ height="15"
196
+ ></v-progress-linear>
197
+ </v-col>
198
+ </v-flex>
199
+ <v-flex xs4></v-flex>
200
+ </v-layout>
201
+ <v-layout class="mt-2" v-if="updateTotals !== 1">
202
+ <v-flex xs8></v-flex>
203
+ <v-flex xs4>
204
+ <v-col :key="updateTotals">
205
+ <p>
206
+ {{
207
+ $t(
208
+ 'plugin.games.components.content.blocks.multiple_choice.total_points'
209
+ )
210
+ }}
211
+ </p>
212
+ <p>
213
+ {{ studentAmountCorrect }}
214
+ {{
215
+ $t(
216
+ 'plugin.games.components.content.blocks.multiple_choice.out_of'
217
+ )
218
+ }}
219
+ {{ totalQuestionsMultipleChoice }}
220
+ </p>
221
+ </v-col>
222
+ </v-flex>
223
+ </v-layout>
224
+ </v-carousel-item>
225
+ <Dialog
226
+ v-model="dialog"
227
+ :trigger="false"
228
+ @click:outside="close"
229
+ @click:close="close"
230
+ @keydown.esc="close"
231
+ >
232
+ <template #title>
233
+ <div v-if="hint">
234
+ {{
235
+ $t(
236
+ 'plugin.games.components.content.blocks.multiple_choice.hint_title'
237
+ )
238
+ }}
239
+ </div>
240
+ <div v-if="answerDescriptionModal">
241
+ {{
242
+ $t(
243
+ 'plugin.games.components.content.blocks.multiple_choice.answer_description'
244
+ )
245
+ }}
246
+ </div>
247
+ </template>
248
+ <template #form="{ on, attrs }">
249
+ <div v-bind="attrs" v-on="on">
250
+ <div v-if="hint">
251
+ {{ hintText }}
252
+ </div>
253
+ <div v-if="answerDescriptionModal">
254
+ {{ answerDescription }}
255
+ </div>
256
+ </div>
257
+ </template>
258
+ </Dialog>
259
+ </v-container>
260
+ </v-carousel>
261
+ </v-col>
262
+ </v-row>
263
+ </v-container>
264
+ </template>
265
+ <script>
266
+ import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
267
+ import Dialog from '~/components/Dialog.vue'
268
+ import _ from 'lodash'
269
+ import Crypto from '~/helpers/Crypto'
270
+ import { mapGetters } from 'vuex'
271
+ import UserContentBlockState from '~/models/UserContentBlockState'
272
+ import ContentBlock from '~/models/ContentBlock'
273
+ import Course from '~/models/Course'
274
+
275
+ export default {
276
+ name: 'MultipleChoice',
277
+ extends: BaseContentBlock,
278
+ components: { Dialog },
279
+ beforeMount() {
280
+ if (_.isEmpty(this.block)) {
281
+ this.block = {}
282
+ }
283
+ if (_.isEmpty(this.block.metadata)) {
284
+ this.block.metadata = {}
285
+ }
286
+ if (_.isEmpty(this.block.metadata.config)) {
287
+ this.block.metadata.config = {}
288
+ }
289
+ if (_.isEmpty(this.block.metadata.config.title)) {
290
+ this.block.metadata.config.title = ''
291
+ }
292
+ if (_.isEmpty(this.block.metadata.config.instructions)) {
293
+ this.block.metadata.config.instructions = ''
294
+ }
295
+ if (_.isEmpty(this.block.metadata.config.questions)) {
296
+ this.block.metadata.config.questions = []
297
+ }
298
+ this.updateTotals = 1
299
+ },
300
+ data() {
301
+ return {
302
+ updateKey: 0,
303
+ completedItems: [],
304
+ dialog: false,
305
+ hint: false,
306
+ hintText: '',
307
+ answerDescriptionModal: false,
308
+ answerDescription: '',
309
+ studentAmountCorrect: null,
310
+ totalQuestionsMultipleChoice: null,
311
+ updateTotals: 1,
312
+ studentResponses: [],
313
+ }
314
+ },
315
+ computed: {
316
+ ...mapGetters({
317
+ organization: 'organization/get',
318
+ course: 'course/get',
319
+ }),
320
+ completedAmount() {
321
+ if (this.completedItems) {
322
+ return _.flatten(this.completedItems).length
323
+ }
324
+ },
325
+ totalAmountQuestions() {
326
+ if (this.block.metadata.config.questions) {
327
+ return _.flatten(this.block.metadata.config.questions).length
328
+ }
329
+ },
330
+ completedPercent() {
331
+ if (
332
+ this.block.metadata.config.questions.length > 0 &&
333
+ this.completedItems.length > 0
334
+ ) {
335
+ return (this.completedAmount / this.totalAmountQuestions) * 100
336
+ }
337
+ return 0
338
+ },
339
+ },
340
+ mounted() {
341
+ this.studentAmountCorrect = 0
342
+ this.onAmountCorrect()
343
+ this.onTotalAmountOfQuestions()
344
+ },
345
+ methods: {
346
+ async onAmountCorrect() {
347
+ let correct = 0
348
+ // grabs user state to get the total amount of questions a studentanswered correctly
349
+ let userState = await UserContentBlockState.where({
350
+ 'metadata->block->tag': 'plugin-games-multiple-choice',
351
+ }).get()
352
+ userState.forEach((state) => {
353
+ state.metadata.studentResponses.forEach((element) => {
354
+ if (element.isStudentCorrect === true) {
355
+ correct = correct + 1
356
+ }
357
+ })
358
+ })
359
+
360
+ // sets total
361
+ this.studentAmountCorrect = correct
362
+ // updates display area of totals
363
+ this.updateTotals = Crypto.id()
364
+ this.updateKey = Crypto.id()
365
+ },
366
+ async onTotalAmountOfQuestions() {
367
+ let multipleChoiceTotalQuestions = 0
368
+ // get total amount of questions via content block route
369
+ let multipleChoiceBlocks = await ContentBlock.where(
370
+ 'tag',
371
+ 'plugin-games-multiple-choice'
372
+ )
373
+ .for(new Course({ id: this.course.id }))
374
+ .get()
375
+
376
+ multipleChoiceBlocks.forEach((element) => {
377
+ element.block.metadata.config.questions.forEach((question) => {
378
+ multipleChoiceTotalQuestions =
379
+ multipleChoiceTotalQuestions + 1
380
+ })
381
+ })
382
+
383
+ this.totalQuestionsMultipleChoice = multipleChoiceTotalQuestions
384
+ },
385
+ onAnswerDescription(question) {
386
+ //launches modal and displays anwer description
387
+ this.dialog = true
388
+ this.answerDescriptionModal = true
389
+ this.answerDescription = question.answer_description
390
+ },
391
+ onChooseAnswer(answer, questionIndex) {
392
+ const answerIndex =
393
+ this.block.metadata.config.questions[
394
+ questionIndex
395
+ ].answer_options.indexOf(answer)
396
+ // set answer,chosen and student_response on block state
397
+ if (_.isEmpty(this.studentResponses[questionIndex])) {
398
+ // clones question block into the correct index in the studentResponses araay
399
+ this.studentResponses[questionIndex] = _.cloneDeep(
400
+ this.block.metadata.config.questions[questionIndex]
401
+ )
402
+ // lets html side know whish answer was chosen, this is important in determine css
403
+ // used on the displayed side
404
+ this.studentResponses[questionIndex].answer_options[
405
+ answerIndex
406
+ ].chosen = true
407
+ this.studentResponses[questionIndex].student_response = answer
408
+
409
+ // get question by index
410
+ const question =
411
+ this.block.metadata.config.questions[questionIndex]
412
+ // ensure the object is not already in the completed items array if not push into the array
413
+ const questionDone = this.completedItems.find(
414
+ (item) => item === question
415
+ )
416
+
417
+ if (!questionDone) {
418
+ this.completedItems.push(question)
419
+ }
420
+ if (answer.correctAnswer === true) {
421
+ this.studentAmountCorrect = this.studentAmountCorrect + 1
422
+ this.studentResponses[questionIndex].isStudentCorrect = true
423
+ }
424
+
425
+ this.updateKey = Crypto.id()
426
+ }
427
+ },
428
+ onIsThisCorrect(answer, questionIndex) {
429
+ // check if student already responded
430
+ if (
431
+ !_.isEmpty(
432
+ this.studentResponses[questionIndex] &&
433
+ this.studentResponses[questionIndex].student_response
434
+ )
435
+ ) {
436
+ // checks if answer is correct and applies class
437
+ if (
438
+ this.studentResponses[questionIndex].student_response
439
+ .correctAnswer === true
440
+ ) {
441
+ if (answer.correctAnswer === true) {
442
+ return 'studentAnswerCorrect'
443
+ } else {
444
+ return ''
445
+ }
446
+ } else if (
447
+ this.studentResponses[questionIndex].student_response
448
+ .correctAnswer !== true
449
+ ) {
450
+ if (answer.correctAnswer === true) {
451
+ return 'correctBorder'
452
+ } else if (
453
+ this.studentResponses[questionIndex].student_response
454
+ .correctAnswer !== true
455
+ ) {
456
+ return 'incorrectBorder'
457
+ }
458
+ }
459
+ }
460
+ },
461
+ getCharacter(question, answer) {
462
+ //gets a letter to preface each answer option
463
+ const letters = ['A', 'B', 'C', 'D']
464
+ const questionIndex =
465
+ this.block.metadata.config.questions.indexOf(question)
466
+ const answerIndex =
467
+ this.block.metadata.config.questions[
468
+ questionIndex
469
+ ].answer_options.indexOf(answer)
470
+ return letters[answerIndex]
471
+ },
472
+ onHint(question) {
473
+ // launches dialog modal
474
+ this.dialog = true
475
+ this.hint = true
476
+ // sets text for modal, cannot just reference question.hint in html because
477
+ // the carousel loads all elements regardless of if they are on a different slide or not
478
+ // when mounted so it always shows the last question if referenced question.hint
479
+ this.hintText = question.hint
480
+ },
481
+ onFifty(question) {
482
+ // disables button so user can't click twice
483
+ question.fiftyFifty = true
484
+ // shuffles array so that the first too answers that are incorrect are not the ones disabled every time
485
+ const shuffledArray = this.shuffle(question.answer_options)
486
+ let counter = 0
487
+ shuffledArray.forEach((element) => {
488
+ if (element.correctAnswer !== true && counter < 2) {
489
+ counter = counter + 1
490
+ element.disabled = true
491
+ }
492
+ })
493
+ // resort array back in order based on id
494
+ shuffledArray.sort((a, b) => a.id - b.id)
495
+ this.updateKey = Crypto.id()
496
+ },
497
+ shuffle(array) {
498
+ let currentIndex = array.length,
499
+ randomIndex
500
+
501
+ // While there remain elements to shuffle.
502
+ while (currentIndex != 0) {
503
+ // Pick a remaining element.
504
+ randomIndex = Math.floor(Math.random() * currentIndex)
505
+ currentIndex--
506
+
507
+ // And swap it with the current element.
508
+ ;[array[currentIndex], array[randomIndex]] = [
509
+ array[randomIndex],
510
+ array[currentIndex],
511
+ ]
512
+ }
513
+
514
+ return array
515
+ },
516
+ close() {
517
+ this.dialog = false
518
+ this.hint = false
519
+ this.answerDescriptionModal = false
520
+ this.hintText = ''
521
+ this.answerDescription = ''
522
+ },
523
+ },
524
+ }
525
+ </script>
526
+ <style scoped>
527
+ .questionBody {
528
+ font-size: 18px;
529
+ }
530
+ .optionOutline {
531
+ border: 2px solid var(--v-primary-base);
532
+ border-radius: 3px;
533
+ }
534
+ .correctBorder {
535
+ border: 3px solid var(--v-success-base);
536
+ }
537
+ .incorrectBorder {
538
+ background-color: var(--v-error-base);
539
+ border: 2px solid var(--v-error-base);
540
+ color: white;
541
+ }
542
+ .studentAnswerCorrect {
543
+ background-color: var(--v-success-base);
544
+ color: white;
545
+ }
546
+ .isCorrect {
547
+ color: var(--v-success-base);
548
+ }
549
+ .isIncorrect {
550
+ color: var(--v-error-base);
551
+ }
552
+ .iconStudentCorrect {
553
+ color: var(--v-success-base);
554
+ border-radius: 25px;
555
+ }
556
+ /* need important below to override vuetify preset width for buttons */
557
+ .hintButton {
558
+ min-width: 20% !important;
559
+ margin-right: 16px;
560
+ }
561
+ .fiftyButton {
562
+ min-width: 20% !important;
563
+ margin-right: 16px;
564
+ }
565
+ </style>