💭

Drag & Drop APIを使って要素の並べ替えを実装する

2022/11/20に公開

どうもスプラトゥーン激ハマりのかずうぉんばっとです!

今回はDrag & Drop APIを使って、要素の並べ替えを実装する方法についてご紹介します。

並べ替えのような複雑な処理はついライブラリに頼りがちですが、自分で実装することで

  • 実装がブラックボックスにならずカスタマイズしやすい
  • 依存ライブラリを減らせる
  • 勉強になる

などメリットがあると思っています。

ものは試しに並べ替えを自分で実装してみましょう!

今回実装したソースはこちら
https://github.com/WombatTechnology/vue-drag-drop-sort-sample


Drag & Drop APIとは?

HTML Drag and Drop APIとは、ブラウザでドラッグ&ドロップを実現するためのAPI群です。

  • ドラッグされる要素(以後 ドラッグ要素)
  • ドロップされる要素(以後 ドロップ要素)

のそれぞれに対して、ドラッグ&ドロップの中で起きる様々なイベントのリスナーを提供しています。

利用できるイベント

実際の実装に入る前に、具体的にどのようなイベントが利用できるか確認しておきましょう。

ドラッグ要素のイベント

  • drag: ドラッグされている時(※ ドラッグしている間繰り返し呼ばれ続ける)
  • dragstart: ドラッグの開始
  • dragend: ドラッグの終了

要素に draggable=”true” をつけることで、@drag@dragstart@dragend が利用できるようになります。
コールバック関数にはDragEvent が引数として渡されます。

<script setup lang="ts">
const drag = (event: DragEvent) => {
  console.log("daragging", event)
}

const dragStart = (event: DragEvent) => {
  console.log("dragStart", event)
}

const dragEnd = (event: DragEvent) => {
  console.log("dragEnd", event)
}
</script>

<template>
  <div
    class="drag-target"
    draggable="true"
    @drag="drag"
    @dragstart="dragStart"
    @dragend="dragEnd"
  >
    Drag Me
  </div>
</template>

ドロップ要素のイベント

  • drageenter: ドラッグ要素がドロップ要素内に入った時
  • dragleave: ドラッグ要素がドロップ要素から抜けたとき
  • dragover: ドラッグ要素がドロップ要素上にある時(※ 繰り返し呼ばれ続ける)
  • drop: ドラッグ要素がドロップ要素上にドロップされたとき

@{イベント名} で各種イベントを取得できます。

引数にはドラッグ要素のイベントと同じDragEventが渡されます。

<script setup lang="ts">
const drop = (event: DragEvent) => {
  console.log("drop", event)
}

const dragenter = (event: DragEvent) => {
  console.log("dropenter", event)
}

const dragleave = (event: DragEvent) => {
  console.log("dropleave", event)
}

const dragover = (event: DragEvent) => {
  console.log("dragover", event)
}
</script>

<template>
  <div class="drag-target" draggable="true">Drag Me</div>
  <div
    class="drop-area"
    @drop.prevent="drop"
    @dragover.prevent="dragover"
    @dragenter.prevent="dragenter"
    @dragleave.prevent="dragleave"
  >
    DropMe
  </div>
</template>

実装

Drag&Drop APIの基本が理解できたので、これを使って並べ替えを実装してみましょう。
今回は以下のような4人の名前の並べ替えすることを考えます。

方針

以下の方針で実装します。
① 各要素をドラッグ要素とし、それぞれの要素の上にドロップ要素を配置

② ドラッグが開始したら、ドラッグ要素のindexをfromIndexとして保存

③ 要素がドロップされたら、ドラッグ要素のindexをドロップ要素のindexに移動
移動した結果、新しいindexが各要素に割り当て直され①に戻る。


① 各要素をドラッグ要素とし、それぞれの要素の上にドロップ要素を配置

dataというReactiveオブジェクトのitemsプロパティに名前の一覧を入れて表示させます。
v-for から各要素のindexを取得できます。
各要素の上にドロップ要素を配置します。

<script setup lang="ts">
import { reactive, ref } from "vue"
const data = reactive({
  items: [
    {
      name: "太郎",
    },
    {
      name: "花子",
    },
    {
      name: "健太",
    },
    {
      name: "愛",
    },
  ],
})
</script>

<template>
  <div v-for="(u, i) in data.items">
     <!-- ドロップ要素 -->
    <div class="drop-area">{{ i }}</div>
    <!-- ドラッグ要素 -->
    <span draggable="true">{{ u.name }}</span>
  </div>
</template>

<style>
.drop-area {
  width: 40px;
  background-color: gray;
  height: 20px;
  color: white;
}
</style>

こんな見た目になります。

② ドラッグが開始されたら、ドラッグ要素のindexをfromIndexとして保存

続いて、ドラッグの開始を実装します。
ドラッグ要素の@dragstartイベントでsaveFromIndexをコールし、dragFromIndexをrefに保存します。

<script setup lang="ts">
...
const dragFromIndex = ref<number | null>(null)
const saveFromIndex = (index: number) => {
  dragFromIndex.value = index
}
</script>

<template>
	
  <div v-for="(u, i) in data.items">
    ...
    <span draggable="true" @dragstart="() => saveFromIndex(i)">{{
      u.name
    }}</span>
  </div>
</template>

③ 要素がドロップされたら、ドラッグ要素のindexをドロップ要素のindexに移動

最後にドロップ処理です。
@dropイベントでドロップされたことを検知し、v-forから渡されたindexをmoveItem関数に引き渡します。
前述のようにドロップを受け付けるために、@dragover.prevent は必須ですので、忘れず書くようにしてください。

<div v-for="(u, i) in data.items">
  <div class="drop-area" @drop="() => moveItem(i)" @dragover.prevent>
    {{ i }}
  </div>
</div>

moveItem関数内では、moveIndex関数を呼び出して、 ドラッグされた要素のIndexをドロップされた要素のIndexに移動させます。

const moveItem = (targetIndex: number) => {
  if (dragFromIndex.value === null) return
  data.items = moveIndex(data.items, dragFromIndex.value, targetIndex)
}

moveIndex関数は配列のユーティリティ関数として実装しました。

/**
 * 配列の要素のインデックスをfrom→toに移動させて返す(非破壊)
 * @param original 元の配列
 * @param from
 * @param to
 * @returns 移動後の配列
 */
export const moveIndex = <T>(original: T[], from: number, to: number) => {
  const arr = [...original]
  const target = arr[from]
  arr.splice(from, 1)
  arr.splice(to, 0, target)
  return arr
}

以上で並べ替えが実装できました 🎉
いい感じに並び変わりますね!

ここからさらに

  • dragenter/dragleave を用いて、ドロップ要素の上にドラッグ要素が来たらスタイルを変更する
  • 一番下の要素の下にも並べ替えできるようにする

など、より実プロダクトで使える形に改善できるかと思います。

チャレンジしてみてください🦑

参考: How to Add Drag and Drop to Your VueJS Project

Discussion