あいつの日誌β

働きながら旅しています。

Vue.draggable を Swappable にしたい

どうした?

英単語やら英会話を覚えたいので WEB 単語帳を作ってみました*1

https://flash-cards-marvelous.firebaseapp.com

その時に単語を並べ換える機能が必要だったのですがそこで Vue.draggable を使ってみました。これは Sorable をベースに Vue component 化したものです。

使ってみたところなかなか便利で、すごい、かんたん、気持ちいい Component だなと思いました。

なんですが DB に保存しようとした時に、一つのアイテムを並べ替えたときに変更されるアイテムがたくさんある事に気づきました。

以下の例のように、先頭のアイテムを一番最後に持っていった場合、以下のように全てのアイテムの位置が変わってしまう。

// sort
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [2, 3, 4, 5, 6, 7, 8, 9, 10, 1]

これだと DB に保存する時にちょっと悩ましい。上記のサービスは Firestore を使っているので配列を丸ごと保存しても良いと思いますが、一つの単語帳にたくさんの単語を登録した場合の事も考えないといけない。

将来的に特定の単語を fav したりする機能をつけようとすると コレクション(もしくはサブコレクション)にして ID を持たせて置いた方が良いのですがそうすると order を各行が持つので上記の例だと並び替えが行われる度に複数のドキュメントの order を update しないといけなかったりします。

どうしたい?

というわけで以下のように Swap したいという気分になりました。

// swap
Before: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
After:  [10, 2, 3, 4, 5, 6, 7, 8, 9, 1]

なんですけどその機能は正当な理由により Vue.draggable にはありません。

github.com

どうする?

ソースコードを読んでいたら以下のようにする事によって Swap ぽい挙動にする事に成功しました。

以下が通常の使い方です

<template>
  <table>
    <draggable v-model="items" tag="tbody" @end="handleDragEnd">
      <tr v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },
  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')
      // update database here
    }
  }
}
</script>

これを以下のように修正しました。

<template>
  <table>
    <draggable v-model="items" tag="tbody" :move="handleMove" @end="handleDragEnd" :options="{animation:500}">
      <tr class="movable" v-for="item in items" :key="item.id">
        <td>{{ item.id }}</td><td>{{ item.name }}</td><td>{{ item.age }}</td>
      </tr>
    </draggable>
  </table>
</template>

<script>
import draggable from "vuedraggable";

export default {
  components: {
    draggable,
  },
  data() {
    const items = [
      { id: 1, name: "Bianka Effertz", age: 37 },
      { id: 2, name: "Mckayla Bogan", age: 20 },
      { id: 3, name: "Estevan Mann", age: 17 },
      { id: 4, name: "Cloyd Ziemann", age: 55 }
    ]
    return { items }
  },

  methods: {
    handleDragEnd() {
      this.$toast.show('dragEnd')

      this.futureItem = this.items[this.futureIndex]
      this.movingItem = this.items[this.movingIndex]
      const _items = Object.assign([], this.items)
      _items[this.futureIndex] = this.movingItem
      _items[this.movingIndex] = this.futureItem

      this.items = _items
    },
    handleMove(e) {
      const { index, futureIndex } = e.draggedContext
      this.movingIndex = index
      this.futureIndex = futureIndex
      return false // disable sort
    }
}
</script>

サンプルはこちら:

fir-vue-draggable.firebaseapp.com

解説

draggable の :move プロパティはそのまま Sortable に渡されます。ここに関数をセットすることができるのですが false を返すと sort の挙動をキャンセルすることができます。

sort の挙動はキャンセルしつつ、ドラッグした状態で drag している item と drop されるであろう場所にある item を保持します。

sort の挙動は抑制されているので this.items は変化しないのですが、@end イベントで this.items を保持していた item を swap させてつじつまを合わせています。

というわけで

無事(?) Vue.draggable を Swap させる挙動に変更できました。とはいえそのそも Soratable が Swap する事を前提にした設計ではないのでべき論でいうと他の Vue component を使ったほうが良いのかもしれません。

というわけで何か良さげなライブラリーがあったらどなたか教えてください。

こちらからは以上です。

*1:肝心の英語は勉強していない