本文書では、vue.jsで作成したアプリケーションのページ内のテーブルの行要素(tr)をDrag&Dropで上下に移動させる方法について説明を行っています。vue.jsにはDrag&Dropを簡単に行えるライブラリが存在していますが、本文書ではライブラリを使用せずにHTML5 Drag and Dropの機能を利用します。入門者でもわかるようにシンプルなコードで作成しているので最後まで読み終えると自分の力でDrag&Dropを実装することが可能です。

Drag&Dropの動作
Drag&Dropの動作

準備

vue.jsはcdnを利用します。またページに簡易的なCSSを適用するためにbootstrapをcdnを経由して利用します。vue.jsとbootstrapの動作確認を行うために下記のコードを記述しブラウザで確認します。

bootstapは必須ではないので装飾が必要でなければ設定する必要はありません。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <title>Vue.js + HTML Drag&Drop</title>
</head>
<body>
  <div id="app" class="container mt-5">
    <h1>{{ message }}</h1>
  </div>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: 'HTML Drag&Drop in vue.js page'
    }
  })
</script>
</body>
</html>
vue.jsの動作確認
vue.jsの動作確認

テーブルの作成

商品情報の入ったlistsの配列をvue.jsのデータプロパティに追加します。追加した配列listsはv-forを使って展開し、テーブル行を作成します。


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <title>Vue.js + HTML Drag&Drop</title>
</head>
<body>
  <div id="app" class="container mt-5">
    <h1>{{ message }}</h1>
    <table class="table table-striped table-bordered">
      <thead>
        <tr class="thead-dark">
          <th>ID</th><th>NAME</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(list, index) in lists">
          <td>{{list.id}}</td>
          <td>{{list.name}}</td>
        </tr>
      </tbody>
    </table>
  </div>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: 'HTML Drag&Drop in vue.js page',
      lists: [
        {
          id: 1, 
          name: 'ProductA'
        },
        {
          id: 2, 
          name: 'ProductB'
        },
        {
          id: 3, 
          name: 'ProductC'
        },
        {
          id: 4, 
          name: 'ProductD'
        },
        {
          id: 5, 
          name: 'ProductE'
        },
      ]
    }
  })
</script>
</body>
</html>
テーブル作成
テーブル作成

ドラッグの設定

現時点の設定ではマウスを利用して行要素をドラッグしようとしても行要素を掴むこともできません。要素を掴むためにはdraggableを要素に設定する必要があります。


<tr 
  v-for="(list, index) in lists"
  draggable
>

draggableをtrに追加することでブラウザ上でtr要素を掴んでドラッグできるようになるので確認を行なってください。要素を掴んでドラッグしてドロップしても何も起こりませんが現時点ではtr要素を掴んでドラッグすることができるようになりました。

ドラッグした要素の情報を取得

イベントを設定し、ドラッグした要素の情報を取得します。ドラッグした要素の情報を取得するために@dragstartイベントを利用します。要素を掴んでドラッグを開始すると@dragstartイベントで設定した関数が実行されます。


<tr 
  v-for="(list, index) in lists"
  draggable
  @dragstart="dragList($event, index)"
>

dragList関数をvue.jsのmethodsに追加します。動作確認のためにドラッグした要素のindexの情報をコンソールログに表示させます。


methods:{
  dragList(event, listIndex){
    console.log(listIndex)
  }
}

実際にブラウザ上で要素をドラッグしてドラッグした要素のindexがコンソールログに表示されればここまでの動作は正常に行われています。

コンソールログでドラッグした要素のindexを確認
コンソールログでドラッグした要素のindexを確認

ドラッグした要素を削除

実際に要素をドラッグしてドロップする際はドラッグした要素が現在ある場所から削除されるのでdragList関数内でドラッグした要素が削除できるか確認してみます。

要素の削除には、JavaScriptのsplice関数を利用します。第一引数には削除したい配列の番号、第2引数に削除する要素の数を指定します。戻り値には削除した要素の情報が保存されるのでdeleteList[0]で削除した要素の情報を取得しています。


methods:{
  dragList(event, dragIndex){
    const deleteList = this.lists.splice(dragIndex,1);
    console.log(deleteList[0]);
  }
}

要素を掴んでドラッグしてみるとドラッグした瞬間に要素が削除されます。5つあった要素のうち2つをドラッグしました。

ドラッグすると要素が消える
ドラッグすると要素が消える

削除した要素の情報がコンソールに表示されます。

削除した要素の情報
削除した要素の情報

ここまで処理ではドラッグのみに注目し要素がドラッグできることとドラッグした要素を削除できることが確認できました。次はドラッグだけではなく

dataTransferインターフェイスの設定

ドロップ後の処理に必要となるドラッグ要素に関する情報を保存するためdataTransferインターフェイスの設定を行います。

dataTransferインターフェイスを使うことでドラッグした要素の情報の保存、ドラッグ&ドロップ操作の制御に関する設定を行うことができます。


methods:{
  dragList(event, dragIndex){
    event.dataTransfer.effectAllowed = 'move'
    event.dataTransfer.dropEffect = 'move'
    event.dataTransfer.setData('drag-index',dragIndex)
  },

dataTransferのsetDataメソッドでドラッグした要素のindexを保存します。setDataで保存したデータはgetDataで取り出すことができます。setDataでは文字列のみ保存することが可能です。

dropEffectではドラッグで行う操作を設定します。デフォルトではnoneに設定され、今回は移動なのでmoveを設定しています。

effectAllowedではドロップ先で行える操作を設定します。デフォルトではuninitializedが設定され、今回は移動なのでmoveを設定しています。

[commment]Chromeを利用して動作確認を行いましたが、dropEffectとeffectAllowedを設定しなくてもdrop&drag動作は行われます。またdropEffectとeffectAllowedを異なる値に設定しても動作しました。しかし、effectAllowedをnoneに設定するとDropさせることができませんでした。[/comment]

Dropの設定

ドラッグした要素の設定が完了したので、ここからはドロップ側の処理を行なっていきます。

ドラッグの時はdragstartイベントを設定しましたが、ドロップの場合はdropイベントを設定します。dropイベントではdropList関数を実行します。


<tr 
  v-for="(list, index) in lists"
  draggable
  @dragstart="dragList($event, index)"
  @drop="dropList($event, index)"
  @dragover.prevent
  @dragenter.prevent
>
dropイベントを動作させるためには、dragoverイベントとdragentgerイベントをpreventする必要があります。もし、この2つの設定がないとdropイベントは実行されません。

methods:{
      dragList(event, dragIndex){
        event.dataTransfer.effectAllowed = 'move'
        event.dataTransfer.dropEffect = 'move'
        event.dataTransfer.setData('drag-index',dragIndex)
      },
      dropList(event, dropIndex){
        const dragIndex = event.dataTransfer.getData('drag-index')
        const deleteList = this.lists.splice(dragIndex, 1);
        this.lists.splice(dropIndex, 0, deleteList[0])
      }
    }
  })

dropListメソッドの中では、eventのdataTransferを使ったドラッグ時にsetDataで保存したドラッグした要素のindexであるdrag-indexを取得します。

そのdrag-indexを利用してドラッグした要素をsplice関数を使って削除し、削除したオブジェクトはdeleteListに保存します。deleteListは配列なので削除した最初の要素をdeleteList[0]で取得することができます。

ドロップした要素のindexであるdropIndexとdeleteList[0]を使って、dropIndexの位置にドラッグした要素を追加します。この時も削除と同じ関数のspliceを利用します。第2が0になっているので削除は行わず、第3引数のオブジェクトを第一引数のdropIndexの場所に追加しています。

テーブル作成
テーブル作成

実際に要素を掴んでドラッグ&ドロップすると下記のように要素の移動を行うことができます。

Drag&Dropで要素が移動
Drag&Dropで要素が移動

最後に今回作成したコードを記載しておきます。


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <title>Vue.js + HTML Drag&Drop</title>
</head>
<body>
  <div id="app" class="container mt-5">
    <h1>{{ message }}</h1>
    <table class="table table-striped table-bordered">
      <thead>
        <tr class="thead-dark">
          <th>ID</th>
          <th>NAME</th>
        </tr>
      </thead>
      <tbody>
        <tr 
          v-for="(list, index) in lists"
          draggable
          @dragstart="dragList($event, index)"
          @drop="dropList($event, index)"
          @dragover.prevent
          @dragenter.prevent
        >
          <td>{{list.id}}</td>
          <td>{{list.name}}</td>
        </tr>
      </tbody>
    </table>
  </div>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: 'HTML Drag&Drop in vue.js page',
      lists: [
        {
          id: 1, 
          name: 'ProductA'
        },
        {
          id: 2, 
          name: 'ProductB'
        },
        {
          id: 3, 
          name: 'ProductC'
        },
        {
          id: 4, 
          name: 'ProductD'
        },
        {
          id: 5, 
          name: 'ProductE'
        },
      ]
    },
    methods:{
      dragList(event, dragIndex){
        event.dataTransfer.effectAllowed = 'move'
        event.dataTransfer.dropEffect = 'move'
        event.dataTransfer.setData('drag-index',dragIndex)
      },
      dropList(event, dropIndex){
        const dragIndex = event.dataTransfer.getData('drag-index')
        const deleteList = this.lists.splice(dragIndex, 1);
        this.lists.splice(dropIndex, 0, deleteList[0])
      }
    }
  })
</script>
</body>
</html>

Order列に順番を保持したい場合

データベースからListsを取得して、行の順番を変更した後に変更した順番をデータベースに保持したい場合には下記のように行うことができます。

配列listsのlistオブジェクトに新しいプロパティorderを追加します。


data: {
  message: 'HTML Drag&Drop in vue.js page',
  lists: [
    {
      id: 1, 
      name: 'ProductA',
      order: 1
    },
    {
      id: 2, 
      name: 'ProductB',
      order: 2
    },
    {
      id: 3, 
      name: 'ProductC',
      order: 3
    },
    {
      id: 4, 
      name: 'ProductD',
      order: 4
    },
    {
      id: 5, 
      name: 'ProductE',
      order: 5
    },
  ]
},

ブラウザにも表示できるように列を一つ追加します。


<table class="table table-striped table-bordered">
  <thead>
    <tr class="thead-dark">
      <th>ID</th>
      <th>NAME</th>
      <th>ORDER</th>
    </tr>
  </thead>
  <tbody>
    <tr 
      v-for="(list, index) in lists"
      draggable
      @dragstart="dragList($event, index)"
      @drop="dropList($event, index)"
      @dragover.prevent
      @dragenter.prevent
    >
      <td>{{list.id}}</td>
      <td>{{list.name}}</td>
      <td>{{list.order}}</td>
    </tr>
  </tbody>
</table>

ブラウザで確認するとOrder列が追加されますが、Drag&Dropで順番を変えてもOrder列の値は変わりません。

Order列を追加
Order列を追加

Drop後にORDER列の設定値を変更できるようにmap関数を使ってORDERの値を設定します。 


dropList(event, dropIndex){
  const dragIndex = event.dataTransfer.getData('drag-index')
  const deleteList = this.lists.splice(dragIndex, 1);
  this.lists.splice(dropIndex, 0, deleteList[0])
  this.lists.map((list,index) => {
    list.order = index + 1
  })
}

設定後、Drag&Dropで行の順番を変更すると順番が変更されることが確認できます。

Order列を更新
Order列を更新