HTML5 Drag and Drop API

この記事では、ドラッグ&ドロップの基本について説明します。

ドラッグ可能なコンテンツを作成する

ほとんどのブラウザでは、テキストの選択範囲、画像、リンクはデフォルトでドラッグ可能です。たとえば、ウェブページ上のリンクをドラッグすると、タイトルと URL を含む小さなボックスが表示されます。このボックスをアドレスバーまたはデスクトップにドロップすると、ショートカットを作成したり、リンクに移動したりできます。他の種類のコンテンツをドラッグ可能にするには、HTML5 Drag and Drop API を使用する必要があります。

オブジェクトをドラッグ可能にするには、その要素に draggable=true を設定します。画像、ファイル、リンク、ファイル、ページ上のマークアップなど、ほとんどのものをドラッグできます。

次の例では、CSS グリッドでレイアウトされた列を並べ替えるインターフェースを作成します。列の基本的なマークアップは次のようになります。各列の draggable 属性は true に設定されています。

<div class="container">
  <div draggable="true" class="box">A</div>
  <div draggable="true" class="box">B</div>
  <div draggable="true" class="box">C</div>
</div>

コンテナ要素とボックス要素の CSS は次のとおりです。ドラッグ機能に関連する CSS は cursor: move プロパティのみです。残りのコードは、コンテナ要素とボックス要素のレイアウトとスタイルを制御します。

.container {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 10px;
}

.box {
  border: 3px solid #666;
  background-color: #ddd;
  border-radius: .5em;
  padding: 10px;
  cursor: move;
}

この時点ではアイテムをドラッグできますが、それ以外は何も起こりません。動作を追加するには、JavaScript API を使用する必要があります。

ドラッグ イベントをリッスンする

ドラッグ プロセスをモニタリングするには、次のいずれかのイベントをリッスンします。

ドラッグ フローを処理するには、なんらかのソース要素(ドラッグが開始される場所)、データ ペイロード(ドラッグされるもの)、ターゲット(ドロップをキャッチする領域)が必要です。ソース要素は、ほとんどの種類の要素にできます。ターゲットは、ユーザーがドロップしようとしているデータを受け入れるドロップゾーンまたはドロップゾーンのセットです。すべての要素をターゲットにできるわけではありません。たとえば、ターゲットは画像にできません。

ドラッグ シーケンスを開始、終了する

コンテンツに draggable="true" 属性を定義したら、dragstart イベント ハンドラをアタッチして、各列のドラッグ シーケンスを開始します。

このコードは、ユーザーが列のドラッグを開始したときに列の不透明度を 40% に設定し、ドラッグ イベントが終了したときに 100% に戻します。

function handleDragStart(e) {
  this.style.opacity = '0.4';
}

function handleDragEnd(e) {
  this.style.opacity = '1';
}

let items = document.querySelectorAll('.container .box');
items.forEach(function (item) {
  item.addEventListener('dragstart', handleDragStart);
  item.addEventListener('dragend', handleDragEnd);
});

結果は次の Glitch デモで確認できます。アイテムをドラッグすると、その不透明度が変化します。ソース要素には dragstart イベントがあるため、this.style.opacity を 40% に設定すると、その要素が移動中の現在の選択であることをユーザーに視覚的にフィードバックします。アイテムをドロップすると、ドロップ動作をまだ定義していなくても、ソース要素の不透明度は 100% に戻ります。

視覚的な手がかりを追加する

ユーザーがインターフェースを操作する方法を理解できるようにするには、dragenterdragoverdragleave イベント ハンドラを使用します。この例では、列はドラッグ可能であるだけでなく、ドロップ ターゲットにもなっています。ドラッグされたアイテムが列の上に保持されているときに境界線を点線にすることで、ユーザーがこのことを理解できるようにします。たとえば、CSS で、ドロップ ターゲットである要素に over クラスを作成できます。

.box.over {
  border: 3px dotted #666;
}

次に、JavaScript でイベント ハンドラを設定し、列がドラッグされたときに over クラスを追加し、ドラッグされた要素が離れたときに削除します。dragend ハンドラでは、ドラッグの終了時にクラスを削除するようにします。

document.addEventListener('DOMContentLoaded', (event) => {

  function handleDragStart(e) {
    this.style.opacity = '0.4';
  }

  function handleDragEnd(e) {
    this.style.opacity = '1';

    items.forEach(function (item) {
      item.classList.remove('over');
    });
  }

  function handleDragOver(e) {
    e.preventDefault();
    return false;
  }

  function handleDragEnter(e) {
    this.classList.add('over');
  }

  function handleDragLeave(e) {
    this.classList.remove('over');
  }

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });
});

このコードには、注目すべき点がいくつかあります。

  • dragover イベントのデフォルトのアクションは、dataTransfer.dropEffect プロパティを "none" に設定することです。dropEffect プロパティについては、このページの後半で説明します。現時点では、drop イベントの発生を防ぐだけです。この動作をオーバーライドするには、e.preventDefault() を呼び出します。同じハンドラで false を返すこともおすすめします。

  • dragenter イベント ハンドラは、dragover ではなく over クラスを切り替えるために使用されます。dragover を使用すると、ユーザーがドラッグしたアイテムを列の上に保持している間、イベントが繰り返し発生し、CSS クラスが繰り返し切り替わります。これにより、ブラウザは不要なレンダリング作業を大量に行うことになり、ユーザー エクスペリエンスに影響する可能性があります。再描画を最小限に抑えることを強くおすすめします。dragover を使用する必要がある場合は、イベント リスナーの調整またはデバウンスを検討してください。

ドロップを完了する

ドロップを処理するには、drop イベントのイベント リスナーを追加します。drop ハンドラでは、ドロップに対するブラウザのデフォルトの動作(通常は迷惑なリダイレクト)を防ぐ必要があります。そのためには、e.stopPropagation() を呼び出します。

function handleDrop(e) {
  e.stopPropagation(); // stops the browser from redirecting.
  return false;
}

新しいハンドラを他のハンドラと一緒に登録してください。

  let items = document.querySelectorAll('.container .box');
  items.forEach(function(item) {
    item.addEventListener('dragstart', handleDragStart);
    item.addEventListener('dragover', handleDragOver);
    item.addEventListener('dragenter', handleDragEnter);
    item.addEventListener('dragleave', handleDragLeave);
    item.addEventListener('dragend', handleDragEnd);
    item.addEventListener('drop', handleDrop);
  });

この時点でコードを実行しても、アイテムは新しい場所に移動しません。そのためには、DataTransfer オブジェクトを使用します。

dataTransfer プロパティには、ドラッグ アクションで送信されたデータが保持されます。dataTransferdragstart イベントで設定され、ドロップ イベントで読み取られるか処理されます。e.dataTransfer.setData(mimeType, dataPayload) を呼び出すと、オブジェクトの MIME タイプとデータ ペイロードを設定できます。

この例では、ユーザーが列の順序を並べ替えられるようにします。そのためには、まずドラッグが開始されたときにソース要素の HTML を保存する必要があります。

function handleDragStart(e) {
  this.style.opacity = '0.4';

  dragSrcEl = this;

  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/html', this.innerHTML);
}

drop イベントでは、ソース列の HTML を、データをドロップしたターゲット列の HTML に設定して、列のドロップを処理します。これには、ユーザーがドラッグ元と同じ列にドロップしていないことを確認することも含まれます。

function handleDrop(e) {
  e.stopPropagation();

  if (dragSrcEl !== this) {
    dragSrcEl.innerHTML = this.innerHTML;
    this.innerHTML = e.dataTransfer.getData('text/html');
  }

  return false;
}

結果は次のデモで確認できます。この操作を行うには、パソコンのブラウザが必要です。Drag and Drop API はモバイルではサポートされていません。列 A を列 B の上にドラッグ&ドロップして、列の位置がどのように変わるかを確認します。

その他のドラッグ プロパティ

dataTransfer オブジェクトは、ドラッグ プロセス中にユーザーに視覚的なフィードバックを提供するプロパティを公開し、各ドロップ ターゲットが特定のデータ型にどのように応答するかを制御します。

  • dataTransfer.effectAllowed は、ユーザーが要素に対して実行できる「ドラッグの種類」を制限します。ドラッグ&ドロップ処理モデルで使用され、dragenter イベントと dragover イベント中に dropEffect を初期化します。このプロパティには、nonecopycopyLinkcopyMovelinklinkMovemovealluninitialized の値を指定できます。
  • dataTransfer.dropEffect は、dragenter イベントと dragover イベント中にユーザーが受け取るフィードバックを制御します。ユーザーがターゲット要素の上にポインタを置くと、ブラウザのカーソルが、コピーや移動など、実行される操作の種類を示します。効果には、nonecopylinkmove のいずれかの値を指定できます。
  • e.dataTransfer.setDragImage(imgElement, x, y) は、ブラウザのデフォルトの「ゴースト画像」フィードバックの代わりに、ドラッグ アイコンを設定できることを意味します。

ファイルのアップロード

この簡単な例では、列をドラッグ元とドラッグ先の両方として使用しています。これは、ユーザーにアイテムの並べ替えを求める UI で発生する可能性があります。ドラッグ ターゲットとソースが異なる要素タイプになることもあります。たとえば、ユーザーが選択した画像をターゲットにドラッグして商品のメイン画像として選択する必要があるインターフェースなどです。

ドラッグ&ドロップは、ユーザーがデスクトップからアプリにアイテムをドラッグできるようにするためによく使用されます。主な違いは drop ハンドラにあります。dataTransfer.getData() を使用してファイルにアクセスするのではなく、ファイルのデータは dataTransfer.files プロパティに格納されます。

function handleDrop(e) {
  e.stopPropagation(); // Stops some browsers from redirecting.
  e.preventDefault();

  var files = e.dataTransfer.files;
  for (var i = 0, f; (f = files[i]); i++) {
    // Read the File objects in this FileList.
  }
}

詳細については、カスタム ドラッグ&ドロップをご覧ください。

その他のリソース