I have 2 Problems I am trying to solve: I am trying to make a unscramble the scripture game for a Bible App.
The problem is I have scriptures that have the same word in them multiple times (for example "In the beginning God created the heavens and the earth" has the in it 3 times). The way I have it set up now works for words that only appear once in the scripture but for the word "the" my code currently will find the first array item that matches "the" and move it somewhere else in the array. For example: If I have the scrambled scripture-- "In beginning God created heavens earth the the the" and I try to drag the first the between In and beginning, it works like expected but, if I try to drag the second the in between create and heavens it moves the first the (the one between In and beginning). How can I change my compare function so it finds the correct the?
I want my array of words/list of items to rearrange themselves as the word is dragged over the next one, not only when the word is dropped. Let me know if you have any ideas about this.
<template>
<div>
<!-- <div :scripture="scripture">{{ scripture }}</div> -->
<ul :scripture="scriptureArray" id="list"></ul>
<div :verse="verse">- {{ verse }}</div>
</div>
</template>
<script>
let vm;
export default {
data() {
return {
dragging: "",
draggedOver: "",
scriptureArray: [],
};
},
props: {
scripture: {
type: String,
required: true,
default: "",
},
verse: {
type: String,
required: true,
default: "",
},
correct: {
type: String,
required: true,
default: "",
}
},
methods: {
renderDraggableItems(scriptureArr) {
let list = document.getElementById("list");
list.innerText = "";
scriptureArr.forEach((word) => {
var node = document.createElement("li");
node.draggable = true;
node.addEventListener("drag", vm.setDragging);
node.addEventListener("dragover", vm.setDraggedOver);
node.addEventListener("dragend", vm.compare);
node.innerText = word;
list.appendChild(node);
});
},
setDragging(e) {
this.dragging = Number.isNaN(parseInt(e.target.innerText))
? e.target.innerText
: parseInt(e.target.innerText);
},
setDraggedOver(e) {
e.preventDefault();
this.draggedOver = Number.isNaN(parseInt(e.target.innerText))
? e.target.innerText
: parseInt(e.target.innerText);
},
compare(e) {
e.preventDefault();
var index1 = this.scriptureArray.indexOf(this.dragging);
var index2 = this.scriptureArray.indexOf(this.draggedOver);
this.scriptureArray.splice(index1, 1);
this.scriptureArray.splice(index2, 0, this.dragging);
console.log("scriptureArray:", this.scriptureArray);
this.renderDraggableItems(this.scriptureArray);
// this way works as long as no 2 words in the scripture are the same (text matching), is there another way?
},
},
mounted() {
vm = this;
this.scriptureArray = this.scripture.split(" ");
this.renderDraggableItems(this.scriptureArray);
},
};
</script>
<style scopped>
#list {
list-style: none;
font-size: 30px;
display: flex;
justify-content: space-evenly;
}
</style>
this component is called like this
<template>
<!-- add a component that shows the scrptures, keeps track of time, and allows unscrabling -->
<div>
<Timer @GameOver="handleGameOver" />
<DraggableSentence
:scripture="scrambledCurrentScripture"
:verse="currentScriptureVerse"
:correct="correctCurrentScripture"
/>
<!-- TODO: create a level for each key in genesis -->
</div>
</template>
where
scrambledCurrentScripture = "the begging In God created the earth heavens and the"
currentScriptureVerse = "genesis 1:1"
correctCurrentScripture = "In the beginning God created the heave and the earth"
There are a number of things which could be improved in what you have:
index
will suffice. (.indexOf()
returns the first element matching the condition - that's why your compare
method fails).Here it is:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
to: "",
from: "",
words: [],
verse: "Genesis 1:1",
correctAnswer: "In the beginning God created the heavens and the earth"
}),
computed: {
isCorrectOrder() {
return this.words.join(' ') === this.correctAnswer;
}
},
methods: {
onDragEnd() {
const word = this.words[this.from];
this.words.splice(this.from, 1);
this.words.splice(this.to, 0, word);
},
},
mounted() {
// basic shuffle
this.words = this.correctAnswer.split(" ")
.map(s => ({s, r: Math.random()}))
.sort((a, b) => a.r > b.r ? -1 : 1)
.map(o => o.s);
},
})
ul {
list-style: none;
font-size: 24px;
display: flex;
justify-content: space-evenly;
padding-left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="( value, index ) in words"
:key="index"
draggable="true"
@drag="from = index"
@dragover.prevent="to = index"
@dragend.prevent="onDragEnd"
v-text="value" />
</ul>
<div :verse="verse">- {{ verse }}</div>
<h4><em v-if="isCorrectOrder">You got it right!</em></h4>
</div>
As far as the second request goes, my advice is to use Vue.draggable, the Vue wrapper around sortable.js, a small yet powerful d&d package using drag & drop HTML5 api, with fallbacks for legacy browsers and touch compatible.
It will also simplify the markup, since you no longer need to specify drag events:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
words: [],
verse: "Genesis 1:1",
correctAnswer: "In the beginning God created the heavens and the earth"
}),
computed: {
isCorrectOrder() {
return this.words.join(' ') === this.correctAnswer;
}
},
mounted() {
this.words = _.shuffle(this.correctAnswer.split(" "));
},
})
.drag-list {
font-size: 24px;
display: flex;
justify-content: space-evenly;
padding-left: 0;
margin: 40px 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="//cdn.jsdelivr.net/npm/sortablejs@1.8.4/Sortable.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0/vuedraggable.umd.min.js"></script>
<div id="app">
<draggable v-model="words" class="drag-list">
<div v-for="( value, index ) in words"
:key="index">{{value}}</div>
</draggable>
<div :verse="verse">- {{ verse }}</div>
<h4><em v-if="isCorrectOrder">You got it right!</em></h4>
</div>
Couldn't help but add a timer. Way too tempting...
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
words: [],
verse: "Genesis 1:1",
correctAnswer: "In the beginning God created the heavens and the earth",
start: performance.now(),
current: performance.now()
}),
computed: {
isCorrectOrder() {
return this.words.join(' ') === this.correctAnswer;
},
timeElapsed() {
return Math.round((this.current - this.start) / 100) / 10;
}
},
methods: {
tick() {
this.current = performance.now();
if (!this.isCorrectOrder) {
setTimeout(this.tick, 100);
}
}
},
mounted() {
this.words = _.shuffle(this.correctAnswer.split(" "));
this.tick();
},
})
.drag-list {
font-size: 24px;
display: flex;
justify-content: space-evenly;
padding-left: 0;
margin: 40px 10px;
}
.flexer {
display: flex;
align-items: center;
}
code {
padding-left: 2rem;
}
code:after {
content: 's'
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="//cdn.jsdelivr.net/npm/sortablejs@1.8.4/Sortable.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0/vuedraggable.umd.min.js"></script>
<div id="app">
<draggable v-model="words" class="drag-list">
<div v-for="( value, index ) in words" :key="index">{{value}}</div>
</draggable>
<div :verse="verse">- {{ verse }}</div>
<div class="flexer">
<h4><em v-if="isCorrectOrder">You got it right!</em></h4>
<code v-text="timeElapsed"></code>
</div>
</div>