はじめに
リストの並べ替えやカンバンボードを実装しようとして、HTML5標準のDrag and Drop APIに手を出した経験はないでしょうか。dragstartやdragoverのイベント地獄、ゴーストイメージの見た目調整、Firefoxだけ挙動が違う……といったハマりどころが多く、思ったより時間を取られるテーマです。
Dragulaは、そうした面倒を1つの関数呼び出しに閉じ込めてくれるJavaScriptライブラリです。公式のキャッチコピーは「Drag and drop so simple it hurts(あまりにシンプルで拍子抜けする)」。実際、要素を渡すだけで並べ替えも複数リスト間の移動も動き出します。
とはいえ、読むより触った方が早いと思います。まずは実際にドラッグして動く様子を見てみましょう。
Dragulaとは
DragulaはJavaScriptだけで動くドラッグ&ドロップライブラリで、特定のフレームワークに依存しません。React・Vue・Svelteはもちろん、素のHTML+JSでもそのまま使えます。ネイティブのDrag and Drop APIを使わず、mousedown/mousemoveベースで要素の複製(ミラー)を動かす独自実装のため、ブラウザ間の挙動差に振り回されにくいのも特徴です。
主な特徴
- APIがシンプル -
dragula(containers)という1行呼び出しだけで、対象要素同士のドラッグ&ドロップが有効になる - フレームワーク非依存 - Vanilla JS実装なので、React/Vue/Svelteなど任意のUIレイヤーと組み合わせられる
- 柔軟な制御フック -
accepts(ドロップ許可判定)、copy(コピーモード)、moves(ドラッグ開始可否)など、挙動を細かくカスタマイズできるオプションが揃っている - イベントベース -
drag・drop・cancel・removeなどのイベントで、ドラッグ操作の各段階にフックできる
インストール
npmまたはyarnでインストールできます。
npm install dragula
yarn add dragula
CDN経由で直接読み込むことも可能です。
<script src="https://unpkg.com/dragula/dist/dragula.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/dragula/dist/dragula.min.css">
Dragulaのサンプルを動かす
まずは1つのリストの中で、カードをドラッグして並べ替えられる最小構成を見てみましょう。dragula([list])のようにコンテナ要素を配列で渡すだけで、その中の子要素がドラッグ可能になります。dropイベントを使えば、並べ替えが終わったタイミングで最新の順序を取得できます。
要点は次の数行です。
import dragula from 'dragula'
const list = document.getElementById('list')
dragula([list]).on('drop', () => {
const order = [...list.children].map((li) => li.textContent)
console.log(order)
})
実際に動かせるサンプルが下です。カードをドラッグして順番を入れ替えると、下の「現在の並び」がリアルタイムで更新されます。
dragula([list])が返すインスタンスはon('drop', ...)のようにイベントを購読でき、ドロップ確定後のDOM順序をそのまま読み取れます。リストの項目を書き換えたり<li>を増やしたりしても、コードを変更せずそのまま動く点も体験してみてください。
基本的な使い方
もっとも基本的な形は、対象コンテナを配列で渡すだけです。
import dragula from 'dragula'
// 1つのコンテナ内での並べ替え
dragula([document.querySelector('.list')])
// 複数コンテナを渡すと、コンテナ間の移動も許可される
dragula([
document.querySelector('#todo'),
document.querySelector('#done'),
])
dragula()はコンテナのDOM要素配列を受け取り、その直下の子要素をドラッグ可能にします。第2引数にオプションオブジェクトを渡すことで、acceptsやcopy、movesといったフックを使った細かい制御ができます。
実践的なユースケース
複数リスト間のカード移動(カンバンボード)
タスク管理ツールでよく見る、カラム間でカードを移動するカンバンボードです。dragula(containers)に複数のコンテナを渡すだけで実現できますが、acceptsオプションを使うと「特定の条件を満たしたカードだけ、特定のカラムに入れる」といった制御が可能です。
dragula([todo, doing, done], {
accepts(el, target) {
// reviewed属性がtrueのカードだけDoneカラムに入れられる
if (target.dataset.col === 'done') {
return el.dataset.reviewed === 'true'
}
return true
},
})
下のサンプルでは、「レビュー済み」ラベルの付いたカードだけがDoneカラムに移動できます。未レビューのカードをDoneにドラッグしても弾かれる挙動を確認してみてください。
acceptsはドラッグ中に何度も呼ばれ、falseを返すとそのコンテナへは入れません。カラムごとの受け入れ条件をアプリのビジネスルールに合わせて変えるだけで、承認フローのようなUIも組み立てられます。
コピーモードで元のリストを残す
コンポーネントパレットからキャンバスへ要素を配置するような、「元のリストは残したまま複製をドロップ先に置きたい」場面があります。Dragulaはcopyオプションに関数を渡すことで、コンテナごとにコピー動作を切り替えられます。
dragula([templates, canvas], {
copy: (el, source) => source === templates,
accepts: (el, target) => target !== templates,
})
copyがtrueを返すコンテナからのドラッグは、元の要素を残したまま複製がドロップ先に追加されます。下のサンプルでは、テンプレート側のカードをキャンバス側にドラッグすると複製され、テンプレート自体は消えません。
copySortSourceオプションをtrueにすると、コピー元のコンテナ内でも並べ替えができるようになります。テンプレート集からのドラッグ&ドロップでレイアウトを組み立てるようなエディタ機能の土台として使えます。
ドロップイベントで並び順を検知する
並べ替えUIは、見た目が動くだけでなく「最終的な順序をどう取得してサーバーに保存するか」が実装の本題になりがちです。Dragulaはdropイベントの発火タイミングでDOMがすでに更新済みなので、el.parentNode.childrenを読むだけで最新順序を取り出せます。
dragula([list]).on('drop', (el, target) => {
const order = [...target.children].map((li) => li.dataset.id)
// order を任意のタイミングで永続化に使う
})
下のサンプルは、ドロップのたびに現在の並び順をID配列として画面に表示します。実際のアプリではこの配列をAPI呼び出しやlocalStorageへの保存に使う形になります。
data-idのようにDOMに持たせたキーをそのまま読み出せるので、Dragula側に順序管理用の状態を持たせる必要がありません。取得したID配列をどこに送るかはアプリ側の自由です。
まとめ
Dragulaは、dragula(containers)という最小限のAPIから始めて、accepts・copy・movesといったオプションで挙動を積み上げていけるドラッグ&ドロップライブラリです。フレームワークに依存しないため、既存のReact/Vueプロジェクトに部分的に組み込むのも、素のHTML+JSで手早くプロトタイプを作るのも得意です。
まずは1つのリストの並べ替えから試し、慣れてきたらカンバンボードやコピーモードのような複雑な挙動に手を伸ばしてみてください。