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.
  }
}

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

その他のリソース