ドラッグ&ドロップ
Drag and Drop
概要(サマリー)
ドラッグ&ドロップとは、要素をつまんだまま移動し、目的の場所で放して操作を完了させる操作方式のことである。「ドラッグ(drag)」は引きずる、「ドロップ(drop)」は落とすという意味で、机の上で書類を手に取り、別のトレイに置く動作をそのまま画面に持ち込んだものだと考えるとよい。
ファイルをフォルダへ移す、タスクカードを「進行中」から「完了」へ動かす、写真をアップロード枠へ放り込む。いずれも「選んでから、どうするか指定する」という2段階を1つの動作にまとめている点が共通している。
以下は、カードを別の枠へ動かせる簡単なデモである。
未着手
完了
詳細解説
ドラッグ&ドロップとは何か
ドラッグ&ドロップは、次の3つの段階でできている。
- つかむ: 対象を押したまま動かし始める
- 運ぶ: どこに置こうとしているかを画面上で示す
- 放す: 置ける場所なら受け入れ、置けない場所なら元に戻す
このうち初心者が忘れがちなのが2番目の「運ぶ」である。つかんで放す処理だけを書くと、利用者は「いまどこに置こうとしているのか」が分からないまま操作することになる。置ける枠の色を変える、隙間を空けて挿入位置を見せる、といった途中の見せ方が、この操作の使いやすさを決める。
HTMLでの基本的な書き方
ブラウザには、ドラッグ&ドロップのための仕組みが最初から用意されている。動かしたい要素に draggable="true" を付け、あとはイベントで処理する。
<div class="item" draggable="true">記事を書く</div>
<div class="zone">ここに置く</div>
var dragging = null;
// つかんだ要素を覚えておく
item.addEventListener('dragstart', function (e) {
dragging = e.target;
});
// これを書かないと「置けない場所」と判定されてしまう
zone.addEventListener('dragover', function (e) {
e.preventDefault();
});
// 放したときに、要素をこの枠へ移動させる
zone.addEventListener('drop', function (e) {
e.preventDefault();
zone.appendChild(dragging);
});
最大のつまずきどころが dragover の e.preventDefault() である。ブラウザの既定動作は「どこにも落とせない」なので、これを打ち消して初めて、その要素が受け入れ先として機能する。「ドラッグはできるのに落とせない」という相談のほとんどは、この1行が抜けていることが原因である。
主なイベントの流れ
関係するDOMイベントは、つかむ側と受ける側で分かれている。
dragstart: つかんだ瞬間(動かす要素側)drag: 動かしている間、繰り返し発生する(動かす要素側)dragenter: 受け入れ先の上に入った(受け側)dragover: 受け入れ先の上にいる間、繰り返し発生する(受け側)dragleave: 受け入れ先から出た(受け側)drop: 受け入れ先で放した(受け側)dragend: 操作が終わった。成功・失敗どちらでも発生する(動かす要素側)
後片付けは dragend に書くとよい。drop にだけ書くと、枠の外で放して操作が中断されたときに、半透明のままの要素が残ってしまう。
ファイルのドラッグ&ドロップ
画面内の要素だけでなく、パソコンのファイルを画面へ放り込む使い方も多い。この場合、放されたファイルは drop イベントの中から取り出せる。
zone.addEventListener('drop', function (e) {
e.preventDefault();
var files = e.dataTransfer.files; // 放り込まれたファイルの一覧
for (var i = 0; i < files.length; i++) {
console.log(files[i].name, files[i].size);
}
});
ここで注意したいのは、ページ全体でも既定動作を止めておくことである。止めないと、枠の外にファイルを落としたとき、ブラウザがそのファイルを開いてしまい、作業中のページから離脱する。画像を1枚落としただけで入力内容が消えた、という事故はこれが原因で起きる。
スマートフォンでの扱い
draggable を使った仕組みは、もともとマウス操作を前提に作られたもので、スマートフォンのタッチ操作では動かないことが多い。タッチでも動かしたい場合は、touchstart / touchmove / touchend を使って自前で位置を計算するか、それに対応したライブラリを使うことになる。
さらにタッチ環境では、指を動かす操作がページのスクロールと競合する。「カードを動かそうとしたらページがスクロールしてしまう」という状態になりやすいため、長押ししてから移動可能にする、専用のつまみ(ハンドル)部分だけをドラッグ対象にする、といった工夫が要る。
代替手段を必ず用意する
ドラッグ&ドロップは、キーボードだけで操作する人、手が震える人、タッチ操作に慣れていない人にとって難しい操作である。この操作だけしか手段がない画面は、使えない人を生んでしまう。
- 「上へ」「下へ」のボタンを添える
- 移動先を選ぶメニューを用意する
- ファイルなら「ファイルを選択」ボタンも併置する
アクセシビリティの指針でも、ドラッグ操作には単一ポインタでの代替手段を用意することが求められている。実装コストは小さいので、最初から両方を用意しておきたい。ユーザビリティの面でも、代替手段があるほうが確実である。
NOVEBLOには、ドラッグ&ドロップの実装手順をまとめたページがある。
AIコーディングとの関係
ドラッグ&ドロップは、AIに頼むと基本形はすぐ出てくる。ただし出てきたコードは「マウスで、画面内の要素を、1つだけ動かす」前提のことが多い。実際の要件を先に伝えるほど手戻りが減る。
- 何を動かすか(画面内の要素か、パソコンのファイルか)
- 並べ替えたいのか、別の場所へ移したいのか
- スマートフォンのタッチ操作に対応するか
- キーボードやボタンによる代替手段が必要か
たとえば「タスクカードを『未着手』『完了』の2つの枠の間で動かせるようにして。ドラッグ中は移動元を半透明にし、受け入れ先の枠は枠線の色を変えて。キーボードでも移動できるよう、各カードに『完了へ移動』ボタンも付けて」と伝えると、実用に近いものになる。
生成後に確認したいのは3点。dragover の preventDefault があるか、dragend で後片付けをしているか、枠外に落としたときの挙動である。ここが抜けていると、動くように見えて操作を途中でやめたときに壊れる。
よくある勘違い
draggable を付ければ動く?
draggable="true" はあくまで「つかめる」宣言にすぎない。受け入れ先で dragover の既定動作を止め、drop で実際に要素を動かす処理を書いて初めて成立する。つかめるのに置けないのは、実装が途中で止まっている状態である。
ドラッグ&ドロップはスマホでもそのまま動く?
draggable を使った実装は、多くのモバイルブラウザのタッチ操作では反応しない。パソコンで動作確認して満足せず、必ず実機のスマートフォンでも触ってみてほしい。
見た目が動いていればデータも移動している?
画面上でカードが別の枠へ移っても、それはDOMの位置が変わっただけである。並び順をサーバーへ保存する処理を書かなければ、リロードすれば元通りになる。「動いた」と「保存された」は別の話である。
ドラッグ&ドロップは常に使いやすい?
直感的な操作だが、対象が小さい、移動距離が長い、置ける場所が分かりにくい、といった条件が重なると一気に難しくなる。項目が数個なら「上へ」「下へ」のボタンのほうが速く確実なこともある。操作方式は目的から選ぶものであって、新しさで選ぶものではない。
まとめ
- ドラッグ&ドロップは「つかむ・運ぶ・放す」の3段階からなる操作方式である
- ブラウザ標準の仕組みでは
draggable="true"と各種イベントで実装する dragoverでpreventDefault()を呼ばないと、どこにも落とせない- 後片付けは
dragendに書く。中断されたときも確実に元へ戻せる - タッチ操作では動かないことが多く、キーボードやボタンによる代替手段も必ず用意する
情報ソース
より詳しくAIに聞いてみよう
- ドラッグ&ドロップの一連のイベントが、どの順番で発生するのかを整理して教えてください。
- dragover で preventDefault が必要な理由を、ブラウザの既定動作の観点から説明してください。
- スマートフォンのタッチ操作でドラッグ&ドロップを実装する方法と、注意点を教えてください。
- ドラッグ&ドロップの代替手段として、どんなUIを用意すればアクセシビリティ上十分といえますか。
- AIに並べ替え機能のコードを生成してもらうとき、並び順をサーバーへ保存する処理まで含めるにはどう指示すればよいですか。