【JavaScript】ドラッグ&ドロップを実装する

javascript-icon JavaScript

こんにちは、かつコーチです。

前回はトースト通知を扱いました。

今回は、タスク管理ツールのカードや並べ替え可能なリストでおなじみのドラッグ&ドロップを、素のJavaScriptで実装していきます。

ドラッグ&ドロップとは?

HTML5 Drag and Drop APIの概要

ブラウザにはdraggable属性と、それに対応する一連のドラッグイベントが標準で用意されています。

  • dragstart:ドラッグを開始した要素で発火
  • dragover:ドロップ先の要素の上をドラッグ中に継続的に発火
  • drop:ドロップ先の要素で、要素が離された瞬間に発火
  • dragend:ドラッグ操作全体が終わったときに発火

ライブラリを使わなくても、これらのイベントを組み合わせるだけで並べ替え可能なリストが作れます。

どんな場面で使われるか

タスク管理ボードのカード移動、画像の並べ替え、ファイルアップロードのドロップエリアなどが代表例です。

今回は「リスト内の項目を、ドラッグで自由に並べ替えられるようにする」という、実務で一番よく使うパターンを作っていきます。

基本の実装手順

手順1:HTMLとCSSでリストを用意する

まずは並べ替え対象のリストを用意します。

<ul id="sortable-list" class="sortable-list">
  <li class="sortable-item" draggable="true">タスクA:デザイン確認</li>
  <li class="sortable-item" draggable="true">タスクB:実装</li>
  <li class="sortable-item" draggable="true">タスクC:レビュー依頼</li>
  <li class="sortable-item" draggable="true">タスクD:公開作業</li>
</ul>
.sortable-list {
  list-style: none;
  padding: 0;
  max-width: 320px;
}
.sortable-item {
  padding: 12px 16px;
  margin-bottom: 8px;
  background: #f3f4f6;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  cursor: grab;
  user-select: none;
}
.sortable-item.dragging {
  opacity: 0.4;
}
.sortable-item.drag-over {
  border-top: 2px solid #2563eb;
}

<li>draggable="true"を付けることで、その要素がドラッグ可能になります。

.draggingはドラッグ中の元要素、.drag-overはドロップ先候補になっている要素の見た目を変えるためのクラスです。

手順2:dragstart・dragendでドラッグ中の状態を管理する

ドラッグしている要素を覚えておくための処理です。

const list = document.getElementById("sortable-list");
let draggedItem = null;

list.addEventListener("dragstart", (e) => {
  draggedItem = e.target;
  e.target.classList.add("dragging");
});

list.addEventListener("dragend", (e) => {
  e.target.classList.remove("dragging");
  draggedItem = null;
});

draggedItemという変数に「今どの要素をドラッグしているか」を保持しておくのが、この後の並べ替え処理のポイントです。

手順3:dragover・dropで並び替えを実行する

ドラッグ中の要素を、どこに差し込むかを決める処理です。

list.addEventListener("dragover", (e) => {
  e.preventDefault(); // これがないとdropイベントが発火しない
  const target = e.target.closest(".sortable-item");
  if (!target || target === draggedItem) return;

  const rect = target.getBoundingClientRect();
  const isAfter = e.clientY > rect.top + rect.height / 2;

  target.parentNode.insertBefore(
    draggedItem,
    isAfter ? target.nextSibling : target
  );
});

マウスのY座標(e.clientY)が、ドロップ先要素の上半分にあるか下半分にあるかを判定し、insertBeforeで要素の挿入位置を決めています。

こうすることで、「カードの上半分にドロップしたら前に、下半分にドロップしたら後ろに挿入される」という自然な並べ替えが実現できます。

よくあるつまずきポイント・エラー対処

つまずき:dragoverでpreventDefaultを忘れてdropが発火しない

私が最初にこの機能を実装したとき、dragoverイベントに何もハンドラを付けずdropだけを実装して、「何度ドロップしてもうんともすんとも言わない」という状態にハマったことがあります。

❌ Before:dragoverでpreventDefaultを呼んでいない

list.addEventListener("drop", (e) => {
  e.preventDefault();
  console.log("ドロップされました"); // ここが実行されない
});

原因を調べるまで、「dropイベントのリスナーの書き方が間違っているのでは」と何度もコードを見直していました。

実際の原因は、ブラウザの初期状態では要素の上に別の要素をドロップする動作自体がデフォルトで禁止されているという、ブラウザ側の仕様にありました。

✅ After:dragoverでもpreventDefaultを呼んでドロップを許可する

list.addEventListener("dragover", (e) => {
  e.preventDefault(); // ドロップを許可する宣言
});

list.addEventListener("drop", (e) => {
  e.preventDefault();
  console.log("ドロップされました"); // 期待通り実行される
});

dragoverは「ドラッグ中の要素がこの要素の上を通過している間、継続的に発火するイベント」で、ここでe.preventDefault()を呼ぶことが「ここへのドロップを受け付けます」という明示的な合図になります。

これをdrop側だけに書いても手遅れで、dragover側にも必ず書く必要がある、というのがこの仕様の分かりにくいところでした。

応用・一歩先の使い方

タッチデバイスでは別対応が必要になる

HTML5 Drag and Drop APIは、実はスマホなどのタッチデバイスでは標準では動作しません。

タッチ操作にも対応したい場合は、touchstarttouchmovetouchendイベントを使って自前でドラッグの挙動を再現するか、Sortable.jsのようなタッチ対応済みのライブラリを検討するのが現実的です。

素のJavaScriptで完全に自前実装する場合は、要素数が増えると管理が複雑になりやすいため、PC専用の管理画面など「マウス操作前提」の場面から採用するのがおすすめです。

ドロップ後の並び順をサーバーに保存する

実務では、並べ替えた結果をそのまま画面に留めるだけでなく、サーバー側に保存するケースがほとんどです。

list.addEventListener("drop", () => {
  const order = Array.from(list.children).map((item) => item.textContent);
  console.log("保存すべき並び順:", order);
  // ここでfetch()等を使ってサーバーに送信する
});

list.childrenから現在のDOMの並び順をそのまま配列化できるので、この配列をAPIに送信すれば並べ替え結果を永続化できます。

fetch APIを使った送信処理については、次回の記事で詳しく扱います。

まとめ

この記事のポイント

  • HTML5 Drag and Drop APIはdraggable属性とdragstartdragendの一連のイベントで構成される
  • ドラッグ中の要素は変数に保持しておき、dragoverでマウス位置に応じた挿入位置を計算する
  • dragovere.preventDefault()を呼ばないとdropイベントが発火しないという仕様に要注意
  • タッチデバイス対応が必要な場合は、別途タッチイベントの実装かライブラリの検討が必要

次に読むべき記事

次回は、スクロールに応じてコンテンツを追加読み込みする「無限スクロール」の実装を扱います。

→ 次の記事:無限スクロールを実装する

タイトルとURLをコピーしました