Search code examples
javascriptarraysvue.jsdrag-and-dropdom-events

Trying to find index of the exact word in an array instead of just the index of the first word that matches


I have 2 Problems I am trying to solve: I am trying to make a unscramble the scripture game for a Bible App.

  1. 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?

  2. 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"

Solution

  • There are a number of things which could be improved in what you have:

    • words in the scrambled version are different than the ones in the correct answer. If this is for some kids, I can picture them growing old, still playing, frustrated for not being able to crack it
    • you shouldn't perform DOM manipulations when using Vue. Not that it doesn't work, but Vue is quite a race car when it comes to handling and updating DOM efficiently. When you're doing it manually, it's a bit like getting down from driver's seat and pushing it down the road (simply update the source array and let Vue handle DOM based on value changes)
    • when handling drag and drop, you need unique identifiers for your elements. Their current 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>