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() を呼び出します。もう 1 つのおすすめの方法は、同じハンドラで 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 で発生します。状況によっては、ドラッグ ターゲットとソースの要素タイプが異なる場合があります。たとえば、インターフェースでは、選択した画像をターゲットにドラッグして、商品のメイン画像として 1 つの画像を選択する必要があります。

ドラッグ&ドロップは、ユーザーがアイテムをデスクトップからアプリケーションにドラッグできるようにするためによく使用されます。主な違いは 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.
  }
}

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

その他のリソース