@windward/games 0.0.2 → 0.0.4

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 (27) hide show
  1. package/components/content/blocks/crosswordPuzzle/Crossword.ts +475 -0
  2. package/components/content/blocks/crosswordPuzzle/CrosswordElements.ts +36 -0
  3. package/components/content/blocks/crosswordPuzzle/CrosswordPuzzle.vue +542 -0
  4. package/components/content/blocks/dragDrop/BucketGame.vue +4 -4
  5. package/components/content/blocks/dragDrop/SortingGame.vue +1 -1
  6. package/components/content/blocks/flashcards/FlashcardSlides.vue +2 -2
  7. package/components/content/blocks/matchingGame/MatchingGame.vue +2 -2
  8. package/components/content/blocks/multipleChoice/MultipleChoice.vue +1 -1
  9. package/components/content/blocks/multipleChoice/QuestionDialog.vue +2 -2
  10. package/components/content/blocks/quizshowGame/QuizShow.vue +2 -2
  11. package/components/content/blocks/slideshow/SlideShow.vue +2 -2
  12. package/components/content/blocks/wordJumble/WordJumble.vue +24 -12
  13. package/components/settings/CrosswordPuzzleSettingsManager.vue +274 -0
  14. package/components/settings/WordJumbleSettingsManager.vue +4 -1
  15. package/i18n/en-US/components/content/blocks/crossword.ts +7 -0
  16. package/i18n/en-US/components/content/blocks/index.ts +2 -0
  17. package/i18n/en-US/components/settings/crossword.ts +6 -0
  18. package/i18n/en-US/components/settings/index.ts +2 -0
  19. package/i18n/en-US/components/settings/word_jumble.ts +1 -1
  20. package/i18n/en-US/shared/content_blocks.ts +1 -0
  21. package/i18n/en-US/shared/settings.ts +1 -0
  22. package/package.json +1 -1
  23. package/plugin.js +22 -1
  24. package/test/blocks/crossword/CrosswordPuzzle.spec.js +49 -0
  25. package/test/settings/BucketGameManager.spec.js +1 -1
  26. package/test/settings/CrosswordPuzzleManager.spec.js +103 -0
  27. package/test/settings/WordJumbleManager.spec.js +2 -2
@@ -0,0 +1,542 @@
1
+ <template>
2
+ <v-container>
3
+ <div>
4
+ <h2>{{ block.metadata.config.title }}</h2>
5
+ <p>{{ block.metadata.config.instructions }}</p>
6
+ </div>
7
+ <div class="d-flex justify-center" v-if="dummyData">
8
+ {{
9
+ $t(
10
+ 'plugin.games.components.content.blocks.crossword.placeholder'
11
+ )
12
+ }}
13
+ </div>
14
+ <!-- I've used this template as guide:
15
+ https://codepen.io/adrianroworth/pen/OpeyZq
16
+ 54 3544 570127 -->
17
+ <div class="crossword-container" ref="crossword-container">
18
+ <div v-if="grid" class="crossword-board-container">
19
+ <v-row>
20
+ <v-col :class="[smallContainer ? 'col-12' : 'col-7']">
21
+ <div class="crossword-board">
22
+ <template v-for="(row, index1) in grid">
23
+ <!-- ROW -->
24
+ <template v-for="(item, index2) in row">
25
+ <input
26
+ v-if="item"
27
+ :key="`row-${index1}-${index2}`"
28
+ :id="index(index1, index2)"
29
+ class="crossword-board__item"
30
+ :class="{
31
+ 'crossword-board__item--valid':
32
+ equalsLetters(
33
+ form[index1][index2],
34
+ item.char
35
+ ),
36
+ }"
37
+ type="text"
38
+ minlength="1"
39
+ maxlength="1"
40
+ :pattern="pattern(item.char)"
41
+ required="required"
42
+ v-model="form[index1][index2]"
43
+ />
44
+ <span
45
+ v-else
46
+ :key="`row-${index1}-${index2}`"
47
+ :id="index(index1, index2)"
48
+ class="crossword-board__item--blank"
49
+ />
50
+ </template>
51
+ <!-- /ROW -->
52
+ </template>
53
+ </div>
54
+ </v-col>
55
+ <v-col
56
+ :class="[
57
+ smallContainer
58
+ ? ['col-12', 'px-5', 'my-5']
59
+ : 'col-4',
60
+ ]"
61
+ >
62
+ <div class="crossword-clues">
63
+ <dl
64
+ v-if="across.length > 0"
65
+ class="crossword-clues__list crossword-clues__list--across"
66
+ >
67
+ <dt class="crossword-clues__list-title">
68
+ {{ this.across }}
69
+ </dt>
70
+ <dd
71
+ class="crossword-clues__list-item"
72
+ v-for="(word, index) in acrossWords"
73
+ :key="`across-${word.word}-${index}`"
74
+ >
75
+ <span class="hightlighted"
76
+ >{{ index + 1 }}. {{ word.clue }}</span
77
+ >
78
+ </dd>
79
+ </dl>
80
+ <dl
81
+ v-if="down.length > 0"
82
+ class="crossword-clues__list crossword-clues__list--down"
83
+ >
84
+ <dt class="crossword-clues__list-title">
85
+ {{ this.down }}
86
+ </dt>
87
+ <dd
88
+ class="crossword-clues__list-item"
89
+ v-for="(word, index) in downWords"
90
+ :key="`down-${word.word}-${index}`"
91
+ >
92
+ <span class="hightlighted"
93
+ >{{ index + 1 }}. {{ word.clue }}</span
94
+ >
95
+ </dd>
96
+ </dl>
97
+ </div>
98
+ </v-col>
99
+ </v-row>
100
+ <v-row v-if="showPlayAgain" class="d-flex justify-center">
101
+ <v-btn size="sm" class="m-3 primary" @click="playAgain">
102
+ {{
103
+ $t(
104
+ 'plugin.games.components.content.blocks.crossword.play_again'
105
+ )
106
+ }}
107
+ </v-btn>
108
+ </v-row>
109
+ </div>
110
+ </div>
111
+ </v-container>
112
+ </template>
113
+
114
+ <script>
115
+ import { Crossword } from './Crossword'
116
+ import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
117
+ import _ from 'lodash'
118
+
119
+ var CrosswordUtils = {
120
+ PATH_TO_PNGS_OF_NUMBERS: 'numbers/',
121
+
122
+ toHtml: function (grid, show_answers) {
123
+ if (grid == null) return
124
+ var html = []
125
+ html.push("<table class='crossword'>")
126
+ var label = 1
127
+ for (var r = 0; r < grid.length; r++) {
128
+ html.push('<tr>')
129
+ for (var c = 0; c < grid[r].length; c++) {
130
+ var cell = grid[r][c]
131
+ var is_start_of_word = false
132
+ var css_class, char
133
+ if (cell == null) {
134
+ char = '&nbsp;'
135
+ css_class = 'no-border'
136
+ } else {
137
+ char = cell['char']
138
+ css_class = ''
139
+ is_start_of_word =
140
+ (cell['across'] &&
141
+ cell['across']['is_start_of_word']) ||
142
+ (cell['down'] && cell['down']['is_start_of_word'])
143
+ }
144
+
145
+ if (is_start_of_word) {
146
+ var img_url =
147
+ CrosswordUtils.PATH_TO_PNGS_OF_NUMBERS + label + '.png'
148
+ html.push(
149
+ "<td class='" +
150
+ css_class +
151
+ "' title='" +
152
+ r +
153
+ ', ' +
154
+ c +
155
+ "' style=\"background-image:url('" +
156
+ img_url +
157
+ '\')">'
158
+ )
159
+ label++
160
+ } else {
161
+ html.push(
162
+ "<td class='" +
163
+ css_class +
164
+ "' title='" +
165
+ r +
166
+ ', ' +
167
+ c +
168
+ "'>"
169
+ )
170
+ }
171
+
172
+ if (show_answers) {
173
+ html.push(char)
174
+ } else {
175
+ html.push('&nbsp;')
176
+ }
177
+ }
178
+ html.push('</tr>')
179
+ }
180
+ html.push('</table>')
181
+ return html.join('\n')
182
+ },
183
+ }
184
+
185
+ export default {
186
+ name: 'CrossWordPuzzle',
187
+ extends: BaseContentBlock,
188
+ props: {
189
+ lang: {
190
+ type: String,
191
+ default: 'EN',
192
+ validator: function (value) {
193
+ // The value must match one of these strings
194
+ return ['EN', 'ES'].indexOf(value) !== -1
195
+ },
196
+ },
197
+ showPlayAgain: {
198
+ type: Boolean,
199
+ default: true,
200
+ },
201
+ },
202
+ beforeMount() {
203
+ if (_.isEmpty(this.block)) {
204
+ this.block = {}
205
+ }
206
+ if (_.isEmpty(this.block.metadata)) {
207
+ this.block.metadata = {}
208
+ }
209
+ if (_.isEmpty(this.block.metadata.config)) {
210
+ this.block.metadata.config = {}
211
+ }
212
+ if (_.isEmpty(this.block.metadata.config.title)) {
213
+ this.block.metadata.config.title = ''
214
+ }
215
+ if (_.isEmpty(this.block.metadata.config.instructions)) {
216
+ this.block.metadata.config.instructions = ''
217
+ }
218
+ this.onSetUpData()
219
+ },
220
+ data() {
221
+ return {
222
+ selectedWords: [],
223
+ acrossWords: [],
224
+ downWords: [],
225
+ matrix: null,
226
+ grid: null,
227
+ form: null,
228
+ smallContainer: false,
229
+ dummyData: false,
230
+ wordsWithReflexions: [],
231
+ dummyWordData: [
232
+ {
233
+ word: 'Coffee',
234
+ description:
235
+ 'Many people drink it in the morning with milk or cream.',
236
+ },
237
+ {
238
+ word: 'Tea',
239
+ description: "British people drink it at 5 o' clock.",
240
+ },
241
+ {
242
+ word: 'Peach',
243
+ description: 'Juicy, round fruit with a stone-like seed.',
244
+ },
245
+ {
246
+ word: 'Grape',
247
+ description: 'You make wine from this fruit.',
248
+ },
249
+ {
250
+ word: 'Primero',
251
+ description:
252
+ 'Que ocupa el lugar número uno en una serie ordenada. Dicho de una persona o de una cosa: Que precede a las demás de su especie en orden, tiempo, lugar, situación, clase o jerarquía.',
253
+ },
254
+ {
255
+ word: 'Lemon',
256
+ description: 'You make lemonade from this fruit.',
257
+ },
258
+ ],
259
+ }
260
+ },
261
+ computed: {
262
+ words: function () {
263
+ return this.selectedWords.map(
264
+ (wordWithReflexion) => wordWithReflexion.word
265
+ )
266
+ },
267
+
268
+ clues: function () {
269
+ return this.selectedWords.map(
270
+ (wordWithReflexion) => wordWithReflexion.description
271
+ )
272
+ },
273
+
274
+ across: function () {
275
+ let acrossWord
276
+ switch (this.lang) {
277
+ case 'ES':
278
+ acrossWord = 'Horizontales'
279
+ break
280
+ default:
281
+ acrossWord = 'Across'
282
+ break
283
+ }
284
+ return acrossWord
285
+ },
286
+
287
+ down: function () {
288
+ let downWord
289
+ switch (this.lang) {
290
+ case 'ES':
291
+ downWord = 'Verticales'
292
+ break
293
+ default:
294
+ downWord = 'Down'
295
+ break
296
+ }
297
+ return downWord
298
+ },
299
+ },
300
+ watch: {
301
+ render(newVal) {
302
+ if (newVal === true) {
303
+ this.onSetUpData(true)
304
+ }
305
+ },
306
+ },
307
+ mounted() {
308
+ this.generateGrid()
309
+ this.$nextTick(function () {
310
+ window.addEventListener('resize', this.getWindowWidth)
311
+ this.getWindowWidth()
312
+ })
313
+ },
314
+ methods: {
315
+ onSetUpData(withoutMount = null) {
316
+ if (_.isEmpty(this.block.metadata.config.words)) {
317
+ this.block.metadata.config.words = []
318
+ this.wordsWithReflexions = this.dummyWordData
319
+ this.dummyData = true
320
+ } else {
321
+ let deleteArray = []
322
+ this.block.metadata.config.words.forEach(function callback(
323
+ value,
324
+ index
325
+ ) {
326
+ if (
327
+ (value.description === '' && value.word === '') ||
328
+ (value.description === null && value.word === null)
329
+ ) {
330
+ deleteArray.push(index)
331
+ }
332
+ })
333
+
334
+ deleteArray.forEach((index) => {
335
+ this.block.metadata.config.words.splice(index)
336
+ })
337
+
338
+ if (this.block.metadata.config.words.length < 2) {
339
+ this.wordsWithReflexions = this.dummyWordData
340
+ this.dummyData = true
341
+ } else {
342
+ this.dummyData = false
343
+ this.wordsWithReflexions = _.cloneDeep(
344
+ this.block.metadata.config.words
345
+ )
346
+ }
347
+ }
348
+ if (withoutMount) {
349
+ this.generateGrid()
350
+ }
351
+ },
352
+ index: function (index1, index2) {
353
+ let id = `item${index1 + 1}-${index2 + 1}`
354
+ return id
355
+ },
356
+
357
+ pattern: function (value) {
358
+ if (value) {
359
+ let pattern = `^[${value.toLowerCase()}${value.toUpperCase()}]{1}$`
360
+ return pattern
361
+ }
362
+ },
363
+
364
+ equalsLetters: function (letter1, letter2) {
365
+ if (letter1 === null) {
366
+ letter1 = ''
367
+ }
368
+ if (letter2 === null) {
369
+ letter2 = ''
370
+ }
371
+ return (
372
+ letter1.toUpperCase() === letter2.toUpperCase() ||
373
+ letter1.toLowerCase() === letter2.toLowerCase()
374
+ )
375
+ },
376
+ setRandomWords: function () {
377
+ let words = this.shuffle(this.wordsWithReflexions)
378
+ this.selectedWords = words
379
+ },
380
+ shuffle(array) {
381
+ let currentIndex = array.length,
382
+ randomIndex
383
+
384
+ // While there remain elements to shuffle.
385
+ while (currentIndex !== 0) {
386
+ // Pick a remaining element.
387
+ randomIndex = Math.floor(Math.random() * currentIndex)
388
+ currentIndex--
389
+
390
+ // And swap it with the current element.
391
+ ;[array[currentIndex], array[randomIndex]] = [
392
+ array[randomIndex],
393
+ array[currentIndex],
394
+ ]
395
+ }
396
+
397
+ return array
398
+ },
399
+ generateGrid: function () {
400
+ const MATRIX_LENGTH = 13
401
+
402
+ let grid
403
+
404
+ while (!grid || grid.length > MATRIX_LENGTH) {
405
+ // // Create crossword object with the words and clues
406
+ this.setRandomWords()
407
+ var cw = new Crossword(this.words, this.clues)
408
+ // create the crossword grid (try to make it have a 1:1 width to height ratio in 10 tries)
409
+ var tries = 10
410
+ grid = cw.getSquareGrid(tries)
411
+ }
412
+
413
+ this.grid = grid
414
+
415
+ this.form = [...Array(MATRIX_LENGTH)].map(() =>
416
+ Array(MATRIX_LENGTH).fill('')
417
+ )
418
+
419
+ // fill grid in
420
+ if (this.grid.length < MATRIX_LENGTH) {
421
+ // add rows
422
+ while (this.grid.length < MATRIX_LENGTH) {
423
+ this.grid.push([])
424
+ }
425
+ // add columns
426
+ this.grid.forEach((row) => {
427
+ while (row.length < MATRIX_LENGTH) {
428
+ row.push(null)
429
+ }
430
+ })
431
+
432
+ let legend = cw.getLegend(this.grid)
433
+ this.acrossWords = legend.across
434
+ this.downWords = legend.down
435
+ }
436
+ },
437
+
438
+ playAgain: function () {
439
+ this.generateGrid()
440
+ },
441
+
442
+ getWindowWidth: function () {
443
+ if (
444
+ this.$refs['crossword-container'].parentElement.parentElement &&
445
+ this.$refs['crossword-container'].parentElement.parentElement
446
+ .clientWidth < 1200
447
+ ) {
448
+ this.smallContainer = true
449
+ }
450
+ },
451
+ },
452
+ beforeDestroy() {
453
+ window.removeEventListener('resize', this.getWindowWidth)
454
+ },
455
+ }
456
+ </script>
457
+
458
+ <style>
459
+ .crossword-board__item--valid {
460
+ background: #9aff67;
461
+ }
462
+
463
+ .hightlighted {
464
+ background: rgba(255, 255, 255, 0.95);
465
+ padding: 3px 5px;
466
+ margin: -3px -5px;
467
+ line-height: 1.7;
468
+ display: inline;
469
+ }
470
+
471
+ .crossword-board-container {
472
+ position: relative;
473
+ /* background: #fff; */
474
+ }
475
+
476
+ .crossword-board {
477
+ position: relative;
478
+ z-index: 1;
479
+ background: transparent;
480
+ border: 1px solid #000;
481
+ width: 650px;
482
+ height: 650px;
483
+ display: grid;
484
+ grid-template: repeat(13, 7.69231%) / repeat(13, 7.69231%);
485
+ list-style-type: none;
486
+ padding: 0;
487
+ margin: 0 auto;
488
+ }
489
+
490
+ .crossword-board__item {
491
+ border: 1px solid #000;
492
+ position: relative;
493
+ z-index: 100;
494
+ text-align: center;
495
+ font-size: 20px;
496
+ font-weight: bold;
497
+ text-transform: uppercase;
498
+ }
499
+
500
+ .crossword-board__item:not(.crossword-board__item--valid) {
501
+ background-color: #fff;
502
+ }
503
+
504
+ .crossword-board__item:active:not(.crossword-board__item--valid),
505
+ .crossword-board__item:focus:not(.crossword-board__item--valid) {
506
+ background: #ffff74;
507
+ border: 1px solid #000;
508
+ outline: 1px solid #000;
509
+ }
510
+
511
+ .crossword-board__item--blank {
512
+ background-color: #75489794;
513
+ border: 1px solid #000;
514
+ outline: 1px solid #000;
515
+ }
516
+
517
+ .crossword-clues {
518
+ /* position: absolute;
519
+ margin-top: 650px; */
520
+ color: #000;
521
+ /* top: 0;
522
+ left: 650px;
523
+ width: 650px; */
524
+ }
525
+
526
+ .crossword-clues__list {
527
+ margin: 0 0 0 60px;
528
+ padding: 0;
529
+ display: inline-block;
530
+ vertical-align: top;
531
+ }
532
+
533
+ .crossword-clues__list-title {
534
+ font-weight: bold;
535
+ padding: 4px;
536
+ }
537
+
538
+ .crossword-clues__list-item {
539
+ margin: 0;
540
+ padding: 4px;
541
+ }
542
+ </style>
@@ -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 }}
@@ -2,7 +2,7 @@
2
2
  <v-container>
3
3
  <v-row>
4
4
  <v-col>
5
- <h1>{{ block.metadata.config.title }}</h1>
5
+ <h3>{{ block.metadata.config.title }}</h3>
6
6
  <p>{{ block.metadata.config.instructions }}</p>
7
7
  </v-col>
8
8
  </v-row>
@@ -36,13 +36,13 @@
36
36
  :rules="validation.textRules"
37
37
  prepend-inner-icon="mdi-comment"
38
38
  ></v-textarea>
39
- <h2>
39
+ <h3>
40
40
  {{
41
41
  $t(
42
42
  'plugin.games.components.settings.multiple_choice.answer_options'
43
43
  )
44
44
  }}
45
- </h2>
45
+ </h3>
46
46
  <p>
47
47
  {{
48
48
  $t(
@@ -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 }}