@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
@@ -0,0 +1,276 @@
1
+ <template>
2
+ <v-form ref="form" v-model="formValid" :key="updateKey">
3
+ <v-row>
4
+ <v-col cols="12">
5
+ <v-textarea
6
+ v-model="question.body"
7
+ :label="
8
+ $t(
9
+ 'plugin.games.components.settings.multiple_choice.question'
10
+ )
11
+ "
12
+ :autofocus="true"
13
+ rows="2"
14
+ :rules="validation.textRules"
15
+ prepend-inner-icon="mdi-help"
16
+ ></v-textarea>
17
+ <v-textarea
18
+ v-model="question.hint"
19
+ :label="
20
+ $t(
21
+ 'plugin.games.components.settings.multiple_choice.question_hint'
22
+ )
23
+ "
24
+ rows="2"
25
+ :rules="validation.textRules"
26
+ prepend-inner-icon="mdi-lightbulb-on-10"
27
+ ></v-textarea>
28
+ <v-textarea
29
+ v-model="question.answer_description"
30
+ :label="
31
+ $t(
32
+ 'plugin.games.components.settings.multiple_choice.answer_description'
33
+ )
34
+ "
35
+ rows="2"
36
+ :rules="validation.textRules"
37
+ prepend-inner-icon="mdi-comment"
38
+ ></v-textarea>
39
+ <h3>
40
+ {{
41
+ $t(
42
+ 'plugin.games.components.settings.multiple_choice.answer_options'
43
+ )
44
+ }}
45
+ </h3>
46
+ <p>
47
+ {{
48
+ $t(
49
+ 'plugin.games.components.settings.multiple_choice.correct_answer'
50
+ )
51
+ }}
52
+ </p>
53
+ </v-col>
54
+ <v-container
55
+ v-for="(answer, index) in question.answer_options"
56
+ :key="index"
57
+ >
58
+ <v-row>
59
+ <v-col
60
+ cols="12"
61
+ md="1"
62
+ class="d-flex justify-center"
63
+ @mouseover="onHover"
64
+ @mouseleave="onHoverLeave"
65
+ >
66
+ <v-checkbox
67
+ v-model="answer.correctAnswer"
68
+ :ref="'checkbox' + index"
69
+ @click="onSetAnswer(answer)"
70
+ ></v-checkbox>
71
+ </v-col>
72
+ <v-col cols="12" md="10">
73
+ <v-textarea
74
+ flat
75
+ solo
76
+ v-model="answer.value"
77
+ :outlined="setOutline(answer)"
78
+ hide-details
79
+ :class="getCorrectAnswer(answer)"
80
+ :label="
81
+ $t(
82
+ 'plugin.games.components.settings.multiple_choice.answer_option'
83
+ )
84
+ "
85
+ rows="2"
86
+ :rules="validation.textRules"
87
+ ></v-textarea>
88
+ </v-col>
89
+ <v-col
90
+ cols="12"
91
+ md="1"
92
+ class="d-flex justify-center"
93
+ @mouseover="onHover"
94
+ @mouseleave="onHoverLeave"
95
+ @click="onDelete(index)"
96
+ >
97
+ <v-icon>mdi-delete</v-icon>
98
+ </v-col>
99
+ </v-row>
100
+ </v-container>
101
+ <v-container class="d-flex justify-center" v-if="overLength">
102
+ <p
103
+ @mouseover="onHover"
104
+ @mouseleave="onHoverLeave"
105
+ @click="onAddAnswer"
106
+ v-on:keyup.enter="onAddAnswer"
107
+ class="fullWidth"
108
+ :class="cursor"
109
+ tabindex="0"
110
+ >
111
+ <v-icon class="primary addIcon">mdi-plus</v-icon>
112
+ {{
113
+ $t(
114
+ 'plugin.games.components.settings.multiple_choice.add_answer'
115
+ )
116
+ }}
117
+ </p>
118
+ </v-container>
119
+ </v-row>
120
+ </v-form>
121
+ </template>
122
+ <script>
123
+ import Form from '~/components/Form'
124
+ import _ from 'lodash'
125
+ import Crypto from '~/helpers/Crypto'
126
+ export default {
127
+ name: 'QuestionDialog',
128
+ extends: Form,
129
+ layout: 'authenticated',
130
+ middleware: ['auth', 'course'],
131
+ props: {
132
+ value: { type: Object, required: false },
133
+ },
134
+ watch: {
135
+ value: {
136
+ deep: true,
137
+ handler(newValue) {
138
+ // clone so that the modal isn't opening with filled in outputs each time
139
+ // if we wrote directly to value it would save on settings side and populate modal when opened
140
+ this.question = _.cloneDeep(newValue)
141
+ if (_.get(this.question, 'answer_options', null) === null) {
142
+ this.question = {
143
+ answer_options: [
144
+ {
145
+ id: 1,
146
+ value: '',
147
+ correctAnswer: true,
148
+ disabled: false,
149
+ },
150
+ {
151
+ id: 2,
152
+ value: '',
153
+ correctAnswer: false,
154
+ disabled: false,
155
+ },
156
+ ],
157
+ body: '',
158
+ hint: '',
159
+ answer_description: '',
160
+ }
161
+ }
162
+ },
163
+ },
164
+ },
165
+ computed: {
166
+ // won't allow more than four answers to be input
167
+ overLength() {
168
+ if (
169
+ this.question.answer_options &&
170
+ this.question.answer_options.length < 4
171
+ ) {
172
+ return true
173
+ } else {
174
+ return false
175
+ }
176
+ },
177
+ },
178
+ data() {
179
+ return {
180
+ question: {},
181
+ validation: {
182
+ textRules: [
183
+ (v) => !!v || this.$t('shared.forms.errors.required'),
184
+ ],
185
+ optionRules: [
186
+ (v) => {
187
+ return (
188
+ !!v ||
189
+ this.$t(
190
+ 'components.content.blocks.assessment.questions.types.multi_choice_single_answer.correct_required'
191
+ )
192
+ )
193
+ },
194
+ ],
195
+ },
196
+ cursor: null,
197
+ updateKey: 0,
198
+ }
199
+ },
200
+ mounted() {
201
+ if (!_.isEmpty(this.value)) {
202
+ this.question = _.cloneDeep(this.value)
203
+ }
204
+ // refreshes data for modal on mount
205
+ this.updateKey = Crypto.id()
206
+ },
207
+ methods: {
208
+ onSave() {
209
+ // clones questions so that input areas aren't linked when reset
210
+ const emittedQuestion = _.cloneDeep(this.question)
211
+ // // emit value from inputs to parent components of the settings manager
212
+ this.$emit('input', emittedQuestion)
213
+ },
214
+ onSaveAndNew() {
215
+ // clones questions so that input areas aren't linked when reset
216
+ const emittedQuestion = _.cloneDeep(this.question)
217
+ // // emit value from inputs to parent components of the settings manager
218
+ this.$emit('input', emittedQuestion)
219
+ this.$emit('saveAndNew')
220
+ },
221
+ onSetAnswer(answer) {
222
+ // changes all inputs that aren't the choosen correct answer to be unchecked
223
+ const index = this.question.answer_options.indexOf(answer)
224
+ this.question.answer_options.forEach((element) => {
225
+ const loopIndex = this.question.answer_options.indexOf(element)
226
+ if (loopIndex !== index) {
227
+ element.correctAnswer = false
228
+ }
229
+ })
230
+ },
231
+ onDelete(index) {
232
+ this.question.answer_options.splice(index, 1)
233
+ },
234
+ onAddAnswer() {
235
+ // pushes new answer object into answer options
236
+ const answerObject = {
237
+ id: this.question.answer_options.length + 1,
238
+ value: '',
239
+ correctAnswer: false,
240
+ disabled: false,
241
+ }
242
+ this.question.answer_options.push(answerObject)
243
+ },
244
+ getCorrectAnswer(answer) {
245
+ // if input area is the correct answer adds a green border
246
+ if (answer.correctAnswer === true) {
247
+ return 'successOutline'
248
+ } else {
249
+ return ''
250
+ }
251
+ },
252
+ setOutline(answer) {
253
+ if (answer.correctAnswer === true) {
254
+ return false
255
+ } else {
256
+ return true
257
+ }
258
+ },
259
+ onHover() {
260
+ this.cursor = 'changePointer'
261
+ },
262
+ onHoverLeave() {
263
+ this.cursor = ''
264
+ },
265
+ },
266
+ }
267
+ </script>
268
+ <style lang="scss" scoped>
269
+ .successOutline {
270
+ border: 4px solid var(--v-success-base);
271
+ color: var(--v-success-base);
272
+ }
273
+ .changePointer {
274
+ cursor: pointer !important;
275
+ }
276
+ </style>
@@ -12,7 +12,7 @@
12
12
  ></v-switch>
13
13
  <div v-if="!tableMode">
14
14
  <div>
15
- <h1
15
+ <h3
16
16
  :aria-label="
17
17
  $t(
18
18
  'plugin.games.components.content.blocks.quizshow_game.title'
@@ -21,7 +21,7 @@
21
21
  tabindex="0"
22
22
  >
23
23
  {{ block.metadata.config.title }}
24
- </h1>
24
+ </h3>
25
25
 
26
26
  <p tabindex="0">
27
27
  <TextViewer v-model="block.metadata.config.instructions" />
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div>
3
3
  <div class="header-description">
4
- <h1
4
+ <h3
5
5
  :aria-label="
6
6
  $t(
7
7
  'plugin.games.components.content.blocks.slideshow.slideshow_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" class="pt-3">
16
16
  {{ block.metadata.config.instructions }}
@@ -0,0 +1,97 @@
1
+ <template>
2
+ <v-container>
3
+ <v-row class="d-flex justify-center">
4
+ <div v-for="(item, index) in letterArray" :key="index">
5
+ <v-text-field
6
+ v-model="item.value"
7
+ :ref="'input' + index"
8
+ :autofocus="index === 0 ? true : false"
9
+ class="textArea ml-1 mr-1"
10
+ maxlength="1"
11
+ @input="onInput($event, index)"
12
+ v-on:keydown="onSpaceHandler($event)"
13
+ ></v-text-field>
14
+ </div>
15
+ </v-row>
16
+ </v-container>
17
+ </template>
18
+ <script>
19
+ import _ from 'lodash'
20
+ export default {
21
+ name: 'Jumble',
22
+ props: {
23
+ value: { type: Object, required: true, default: '' },
24
+ reveal: { type: Boolean, required: true, default: false },
25
+ reset: { type: Boolean, required: true, default: false },
26
+ },
27
+ watch: {
28
+ reveal: {
29
+ handler(newValue) {
30
+ // reveals correct answer in input areas
31
+ if (newValue === true) {
32
+ for (let i = 0; i < this.value.value.length; i++) {
33
+ const letter = this.answer.charAt(i)
34
+ this.letterArray[i].value = letter
35
+ }
36
+ } else {
37
+ this.splitUpWord()
38
+ }
39
+ },
40
+ immediate: true,
41
+ },
42
+ reset: {
43
+ handler(newValue) {
44
+ // removes students answer from input if they got it correct
45
+ this.splitUpWord()
46
+ }
47
+ }
48
+ },
49
+ data() {
50
+ return {
51
+ saveState: false,
52
+ letterArray: [],
53
+ answer: '',
54
+ }
55
+ },
56
+ mounted() {
57
+ this.letterArray = []
58
+ this.answer = this.value.value
59
+ this.splitUpWord()
60
+ },
61
+ methods: {
62
+ splitUpWord() {
63
+ this.letterArray = []
64
+ // gets length of word and creates input area for each letter
65
+ if (!_.isEmpty(this.value.value)) {
66
+ for (let i = 0; i < this.value.value.length; i++) {
67
+ let defaultObject = {
68
+ value: '',
69
+ }
70
+ this.letterArray.push(defaultObject)
71
+ }
72
+ }
73
+ },
74
+ onSpaceHandler(event) {
75
+ // prevents empty space from being entered
76
+ if (event.keyCode === 32) {
77
+ event.preventDefault()
78
+ return false
79
+ }
80
+ },
81
+ onInput(event, index) {
82
+ // handles focusing on next element after student enters a letter
83
+ if (!_.isEmpty(event) && this.$refs['input' + (index + 1)]) {
84
+ this.$refs['input' + (index + 1)][0].focus()
85
+ } else if (index !== 0 && _.isEmpty(event)) {
86
+ this.$refs['input' + (index - 1)][0].focus()
87
+ }
88
+ this.$emit('input', this.letterArray)
89
+ },
90
+ },
91
+ }
92
+ </script>
93
+ <style scoped>
94
+ .textArea {
95
+ width: 20px;
96
+ }
97
+ </style>
@@ -0,0 +1,239 @@
1
+ <template>
2
+ <v-container>
3
+ <v-col class="pa-0">
4
+ <h3>
5
+ {{
6
+ block.metadata.config.title
7
+ ? block.metadata.config.title
8
+ : $t(
9
+ 'plugin.games.components.content.blocks.word_jumble.title'
10
+ )
11
+ }}
12
+ </h3>
13
+ <p>{{ block.metadata.config.instructions }}</p>
14
+ </v-col>
15
+ <v-col class="pa-0">
16
+ <template>
17
+ <v-carousel @change="onSlideChanged()">
18
+ <v-carousel-item
19
+ v-for="(word, index) in block.metadata.config.words"
20
+ :key="index"
21
+ >
22
+ <v-row class="d-flex justify-center outline ma-2">
23
+ <p class="pa-3 mb-0 clueAndJumble">
24
+ <span class="spanBold"
25
+ >{{
26
+ $t(
27
+ 'plugin.games.components.content.blocks.word_jumble.clue'
28
+ )
29
+ }}
30
+ </span>
31
+ {{ word.hint }}
32
+ </p>
33
+ </v-row>
34
+ <v-row class="d-flex justify-center outline ma-2">
35
+ <p class="pa-3 mb-0 clueAndJumble">
36
+ {{ shuffle(word.value) }}
37
+ </p>
38
+ </v-row>
39
+ <v-container :key="'feedback'" :class="feedbackStatus">
40
+ <br />
41
+ <v-row
42
+ class="d-flex justify-space-around"
43
+ align="center"
44
+ justify="center"
45
+ >{{ feedback }}
46
+ </v-row>
47
+ <br />
48
+ </v-container>
49
+ <v-row class="justify-center mt-4">
50
+ <Jumble
51
+ :value="word"
52
+ :reveal="showAnswer"
53
+ :reset="resetValue"
54
+ @input="getResponse($event)"
55
+ ></Jumble>
56
+ </v-row>
57
+ <v-row class="justify-center mt-8">
58
+ <v-btn
59
+ color="primary mr-4"
60
+ @click="onCheckAnswer(word)"
61
+ >{{
62
+ $t(
63
+ 'plugin.games.components.content.blocks.word_jumble.check'
64
+ )
65
+ }}</v-btn
66
+ >
67
+ <v-btn
68
+ color="primary ml-4"
69
+ @click="onRevealAnswer"
70
+ >{{
71
+ $t(
72
+ 'plugin.games.components.content.blocks.word_jumble.reveal'
73
+ )
74
+ }}</v-btn
75
+ >
76
+ </v-row>
77
+ </v-carousel-item>
78
+ </v-carousel>
79
+ </template>
80
+ </v-col>
81
+ </v-container>
82
+ </template>
83
+ <script>
84
+ import _ from 'lodash'
85
+ import Jumble from './Jumble.vue'
86
+ import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
87
+
88
+ export default {
89
+ name: 'WordJumble',
90
+ extends: BaseContentBlock,
91
+ components: {
92
+ Jumble,
93
+ },
94
+ beforeMount() {
95
+ if (_.isEmpty(this.block)) {
96
+ this.block = {}
97
+ }
98
+ if (_.isEmpty(this.block.metadata)) {
99
+ this.block.metadata = {}
100
+ }
101
+ if (_.isEmpty(this.block.metadata.config)) {
102
+ this.block.metadata.config = {}
103
+ }
104
+ if (_.isEmpty(this.block.metadata.config.title)) {
105
+ this.block.metadata.config.title = ''
106
+ }
107
+ if (_.isEmpty(this.block.metadata.config.instructions)) {
108
+ this.block.metadata.config.instructions = ''
109
+ }
110
+ if (_.isEmpty(this.block.metadata.config.feedback_correct)) {
111
+ this.block.metadata.config.feedback_correct = ''
112
+ }
113
+ if (_.isEmpty(this.block.metadata.config.feedback_incorrect)) {
114
+ this.block.metadata.config.feedback_incorrect = ''
115
+ }
116
+ if (_.isEmpty(this.block.metadata.config.words)) {
117
+ this.block.metadata.config.words = []
118
+ }
119
+ },
120
+ data() {
121
+ return {
122
+ saveState: false,
123
+ feedback: this.$t(
124
+ 'plugin.games.components.content.blocks.word_jumble.feedback'
125
+ ),
126
+ showAnswer: false,
127
+ resetValue: false,
128
+ studentResponse: '',
129
+ feedbackStatus: '',
130
+ }
131
+ },
132
+ mounted() {
133
+ this.showAnswer = false
134
+ },
135
+ methods: {
136
+ shuffle(str) {
137
+ var a = str
138
+ var newArr = []
139
+ var neww = ''
140
+ var text = a.replace(/[\r\n]/g, '').split(' ')
141
+
142
+ text.map(function (v) {
143
+ v.split('').map(function () {
144
+ var hash = Math.floor(Math.random() * v.length)
145
+ neww += v[hash]
146
+ v = v.replace(v.charAt(hash), '')
147
+ })
148
+ newArr.push(neww)
149
+ neww = ''
150
+ })
151
+ var x = newArr.map((v) => v.split('').join(' ')).join('\n')
152
+ str = x
153
+ .split('')
154
+ .map((v) => v.toUpperCase())
155
+ .join('')
156
+ return str
157
+ },
158
+ getResponse(event) {
159
+ // child component emits event that triggers this function to get the students response as they enter it
160
+ this.studentResponse = ''
161
+ event.forEach((element) => {
162
+ this.studentResponse = this.studentResponse + element.value
163
+ })
164
+ },
165
+ onRevealAnswer() {
166
+ // reveal prop changed to true to show answer
167
+ this.showAnswer = true
168
+ },
169
+ onCheckAnswer(word) {
170
+ this.studentResponse = this.studentResponse.toLowerCase()
171
+ const answer = word.value.toLowerCase()
172
+ if (this.studentResponse === answer) {
173
+ // updates class
174
+ this.feedbackStatus = 'success'
175
+ // gets custom or standard feedback
176
+ if (
177
+ !_.isEmpty(this.block.metadata.config.feedback_correct) &&
178
+ this.block.metadata.config.feedback_correct !== ''
179
+ ) {
180
+ this.feedback = this.block.metadata.config.feedback_correct
181
+ } else {
182
+ this.feedback = this.$t(
183
+ 'plugin.games.components.content.blocks.word_jumble.correct'
184
+ )
185
+ }
186
+ } else {
187
+ // updates class
188
+ this.feedbackStatus = 'error'
189
+ // gets custom or standard feedback
190
+ if (
191
+ !_.isEmpty(this.block.metadata.config.feedback_incorrect) &&
192
+ this.block.metadata.config.feedback_incorrect !== ''
193
+ ) {
194
+ this.feedback =
195
+ this.block.metadata.config.feedback_incorrect
196
+ } else {
197
+ this.feedback = this.$t(
198
+ 'plugin.games.components.content.blocks.word_jumble.incorrect'
199
+ )
200
+ }
201
+ }
202
+ },
203
+ onSlideChanged() {
204
+ // this function is called when the slide is changed
205
+ // reset the game each time this occurs via props due to fact
206
+ // that components do not remount on slides each time the slide is revisited
207
+ // updates class
208
+ this.feedbackStatus = ''
209
+ this.feedback = this.$t(
210
+ 'plugin.games.components.content.blocks.word_jumble.feedback'
211
+ )
212
+ // ensure answer no longder revealed
213
+ this.showAnswer = false
214
+ // if student has entered a response the input values are reset to empty on slide change
215
+ this.resetValue = !this.resetValue
216
+ },
217
+ },
218
+ }
219
+ </script>
220
+ <style lang="scss" scoped>
221
+ .outline {
222
+ border: 2px solid black;
223
+ border-radius: 10px;
224
+ }
225
+ .clueAndJumble {
226
+ font-size: 20px;
227
+ }
228
+ .spanBold {
229
+ font-weight: 900;
230
+ }
231
+ .error {
232
+ border: dashed 2px #dc3d1a;
233
+ background-color: #fff1f1;
234
+ }
235
+ .success {
236
+ border: dashed 2px #76b778;
237
+ background-color: #f1fff3;
238
+ }
239
+ </style>