😽

【イラスト付き】ドラッグ&ドロップAPI【利用方法】

2024/09/04に公開

はじめに

皆さんこんにちは。
今回はドラッグ&ドロップAPIをご紹介します。

ドラッグ&ドロップAPIはブラウザでのドラッグ&ドロップ操作の細かい制御ができます。今回はドラッグ&ドロップAPIの基本的な使い方について扱います。

こんな人にオススメ

  • ドラッグ&ドロップAPIの概要が知りたい
  • ドラッグ&ドロップAPIの書き方が知りたい

初めて学習する方にも分かるように、要点を絞って丁寧に解説していきます。

😋 ドラッグ&ドロップAPIの全体像をご紹介します♪

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

まずポイントをチェック

  • 要素をドラッグ&ドロップで操作が可能
  • ドラッグ&ドロップ時の細かな挙動の設定が可能
    • フィードバック画像
    • ドロップ先の制御
  • OSからファイルのアップロードが可能

ドラッグ&ドロップAPIとは、ブラウザ上でドラッグ&ドロップを行うための機能です。ドラッグ&ドロップはWebページ内の要素だけでなく、OSからファイルをドロップすることも可能です。

ドラッグ&ドロップ可能になることでユーザビリティの向上が期待できます。また、ドロップ先の制限やフィードバック画像の設定などドラッグ&ドロップ時の細かなカスタマイズが可能になります。

😋 ドラッグ&ドロップでの操作が可能になります♪

シンプルな操作ステップ

まずポイントをチェック

  • ステップ1:draggable属性でドラッグ可能に設定
  • ステップ2:dragstartイベントでDataTransferに値をセット
  • ステップ3:dropイベントでDataTransferから値を取得

ドラッグ&ドロップ操作のシンプルな操作ステップをご紹介します。

ステップ1:draggable属性でドラッグ可能に設定

要素をドラッグ可能にするにはdraggable属性にtrueを指定します。

ステップ2:dragstartイベントでDataTransferに値をセット

ドラッグした要素の情報をドロップ先に伝えるにはDataTransferオブジェクトを使います。DataTransferオブジェクトはドラッグ要素とドロップ先要素で共有されるデータの置き場所です。

要素をドラッグすると、その要素でdragstartイベントが発生します。このタイミングでDataTransferオブジェクトにデータを渡します。

ステップ3:dropイベントでDataTransferから値を取得

ドロップ先ではDataTransferオブジェクトから値を取得し、DOM操作など必要な操作を行います。要素がドロップされると、ドロップ先でdropイベントが発生します。このタイミングでDataTransferオブジェクトからデータを取得します。

😋 DataTransferオブジェクトで値の受け渡しをします♪

発生するイベントと発生タイミング

まずポイントをチェック

  • ドラッグしてる要素で発生するイベント
    • dragstart:ドラッグ開始時
    • drag:ドラッグしてる間に何度も
    • dragend:ドラッグが終了した時
  • ドロップ先の要素で発生するイベント
    • dragleave:ドロップ先から離れると
    • dragenter:ドロップ先に入ると
    • dragover:ドロップ先に乗ってる間に何度も
    • drop:ドロップした時

ドラッグ&ドロップの過程で様々なイベントが発生します。各イベントの発生タイミングをご紹介します。

ドラッグしてる要素で発生するイベント

  • dragstart
    • 発生タイミング:要素のドラッグを開始した時に発生
    • 利用例:DataTransferオブジェクトにデータをセットしたり、ドラッグ開始時のスタイル適用を行うなど
  • drag
    • 発生タイミング:要素をドラッグしてる間に繰り返し発生
    • 利用例:あまり利用しない(ドラッグ開始時に設定した表示を更新するなど)
  • dragend
    • 発生タイミング:要素を放し、ドラッグが終了した時に発生
    • 利用例:あまり利用しない(ドラッグ時に適用したスタイルのリセットなど、ドラッグ操作後の後処理が必要であれば)

ドロップ先の要素で発生するイベント

  • dragenter
    • 発生タイミング:ドラッグ中の要素がドロップ先に入った時に発生
    • 利用例:あまり利用しない(ドロップ先に入った際にドロップ先のスタイルを変更するなど)
  • dragleave
    • 発生タイミング:ドラッグ中にドロップ先の上から離れた時に発生
    • 利用例:あまり利用しない(dragenter時に設定したスタイルなどをリセットするなど)
  • dragover
    • 発生タイミング:ドラッグ中にドロップ先の上に重なってる間に繰り返し発生
    • 利用例:ドロップを許可するためにevent.preventDefault()を実行する
  • drop
    • 発生タイミング:ドラッグ中の要素がドロップ先にドロップされた時に発生
    • 利用例:DataTransferオブジェクトからデータを取得してDOM操作などの処理をする
サンプルコード(drag-and-drop-sample/01.dnd-events/app.js)

const star = document.querySelector('h1');
const box = document.querySelector('div');

// ドラッグ要素で発生するイベント
star.addEventListener('dragstart', (event) => {
    console.log(event.type);
});

star.addEventListener('drag', (event) => {
    // ドラッグ中反復的に発生しコンソールがメッセージだらけになるのでコメントアウト
    // console.log(event.type);
});

star.addEventListener('dragend', (event) => {
    console.log(event.type);
});

// ドロップ先で発生するイベント
box.addEventListener('dragenter', (event) => {
    console.log(event.type);
});

box.addEventListener('dragleave', (event) => {
    console.log(event.type);
});

box.addEventListener('dragover', (event) => {
    event.preventDefault();
    console.log(event.type);
});

box.addEventListener('drop', (event) => {
    console.log(event.type);
});

😋 発生するイベント全てで処理をする必要はありません♪

シンプルなコード例

まずポイントをチェック

  • ステップ1:draggable属性でドラッグ可能に設定
  • ステップ2:dragstartイベントでDataTransferに値をセット
  • ステップ3:dropイベントでDataTransferから値を取得
    • dragoverイベントではpreventDefaultのみ実施

ドラッグ&ドロップ操作のシンプルなコード例を、操作ステップに沿ってご紹介します。

ステップ1:draggable属性でドラッグ可能に設定

要素をドラッグ可能にするにはdraggable属性にtrueを指定します。

<h1 draggable="true"></h1>

ステップ2:dragstartイベントでDataTransferに値をセット

要素をドラッグしたタイミングで受け渡したいデータをセットします。dragstartイベント発生時のEventオブジェクトに含まれるdataTransferプロパティがDataTransferオブジェクトです。setDataメソッドを使いデータをセットします。

setDataメソッド

  • 第一引数:データの形式
  • 第二引数:受け渡したいデータ

データをセットする際にデータの形式を指定します。このデータの形式はドロップ時にデータを取り出す際のキーになります。

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', 'data');
});

ステップ3:dropイベントでDataTransferから値を取得

ドラッグ時にセットした値を取り出します。dropイベント発生時のEventオブジェクトに含まれるdataTransferプロパティがDataTransferオブジェクトです。getDataメソッドを使いデータを取得します。なおdropイベントではドロップ時のデフォルトの動作をキャンセルする必要があるためpreventDefaultを実行します。デフォルトの動作とは画像がドロップされたら新しいタブで開くなどです。

getDataメソッド

  • 第一引数:データの形式

データをセットした際に指定したデータ形式の文字列が、データを取り出す際のキーです。

box.addEventListener('dragover', (event) => {
    event.preventDefault();
});

box.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    console.log(data);
});
サンプルコード全体像(drag-and-drop-sample/02.simple-step/app.js)

const star = document.querySelector('h1');
const box = document.querySelector('div');

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', 'data');
});

box.addEventListener('dragover', (event) => {
    event.preventDefault();
});

box.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    console.log(data);
});

😋 dragstart、dragover、dropで処理をします♪

コピーと移動

まずポイントをチェック

  • コピー:ドロップ時に新しく要素を作成して追加
  • 移動:ドロップ時に既存の要素を取得し追加

dropイベント発生時の処理の例として、ドラッグ要素のコピーと移動の方法をご紹介します。

コピー:ドロップ時に新しく要素を作成して追加

ドラッグ要素のコピーは、DataTransferから取得したデータを元に、要素を新たに生成することで実現します。

  • サンプルの処理内容
    1. DataTransferからドラッグ要素のid属性の値を受け取る
    2. id属性を指定してドラッグ要素の要素オブジェクトを取得する
    3. ドラッグ要素からタグ名を取得し、新しい要素を生成する
    4. ドラッグ要素からコンテントを取得し、生成した要素に渡す
    5. ドロップ先要素に生成した要素を追加し、画面に表示する
copyBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain'); // 1
    const droppedElement = document.getElementById(data); // 2
    const newElement = document.createElement(droppedElement.tagName); // 3
    newElement.textContent = droppedElement.textContent; // 4
    event.target.appendChild(newElement); // 5
});

移動:ドロップ時に既存の要素を取得し追加

ドラッグ要素の移動は、ドラッグ元の要素を取得しDOM操作で別の場所に追加するだけです。

  • サンプルの処理内容
    1. DataTransferからドラッグ要素のid属性の値を受け取る
    2. id属性を指定してドラッグ元の要素オブジェクトを取得する
    3. ドロップ先要素に取得したドラッグ要素を追加し、画面に表示する
moveBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    event.target.appendChild(document.getElementById(data));
});
サンプルコード全体像(drag-and-drop-sample/03.copy-move/app.js)

const star = document.querySelector('#star');
const copyBox = document.querySelector('#copy-box');
const moveBox = document.querySelector('#move-box');

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', event.target.id);
});

// コピー
copyBox.addEventListener('dragover', (event) => {
    event.preventDefault();
});

copyBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    const droppedElement = document.getElementById(data);
    const newElement = document.createElement(droppedElement.tagName);
    newElement.textContent = droppedElement.textContent;
    newElement.style.margin = 0;
    event.target.appendChild(newElement);
});

// 移動
moveBox.addEventListener('dragover', (event) => {
    event.preventDefault();
});

moveBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    event.target.appendChild(document.getElementById(data));
});

😋 コピーは別途生成、移動は再利用で実現します♪

ドロップ先の制限

まずポイントをチェック

  • dragstartの時にeffectAllowedに操作を指定(何をするか)
  • dragoverの時にdropEffectに許可する操作を指定(何をするならOKか)

ドラッグ&ドロップ操作を行う際、ドロップ可能な操作を限定することができます。

|🍕 例えば、コピーのみを許可したり、移動のみを許可するなどです。これにより誤った操作や想定外の操作を防止することができます。

DataTransferオブジェクトのeffectAllowedプロパティとdropEffectプロパティに許可する操作を指定します。それぞれの値が対応している場合のみ、ドロップを受け付けます。
指定可能な値と設定方法は次の通りです。

effectAllowed

effectAllowedの値 操作内容
none ドロップ不可
all すべての操作
uninitialized デフォルト(all と同等)
copy コピー操作
move 移動操作
link リンクのドロップ操作
copyLink コピー操作とリンクのドロップ操作
copyMove コピー操作と移動操作
linkMove リンクのドロップ操作と移動操作

effectAllowedはdragstartイベントの処理で設定します。ドラッグ対象の要素が行う操作を指定します。

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', event.target.id);
    event.dataTransfer.effectAllowed = 'copy'; // 要素のコピーを許可する
});

dropEffect

dropEffectの値 許可する操作内容
デフォルトのnone 全てのドロップを許可
none ドロップ不可
copy コピー操作を許可
move 移動操作を許可
link リンクのドロップを許可

dropEffectはdragoverイベントの処理で設定します。ドロップ時に許可する処理を指定します。

copyBox.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy'; // 要素のコピーを許可する
});

effectAllowedとdropEffectの対応関係

| dropEffectの値 effectAllowedの値|
| ---- | ---- |
| デフォルトのnone | 全て許可|
| none | 全て禁止|
| copy | copy, copyLink, copyMove, all, uninitialized|
| move | move, copyMove, linkMove, all, uninitialized|
| link | link, copyLink, linkMove, all, uninitialized|

サンプルコード 星の移動のみ許可(drag-and-drop-sample/04.effect-allowed/app.js)
const star = document.querySelector('#star');
const copyBox = document.querySelector('#copy-box');
const moveBox = document.querySelector('#move-box');

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setData('text/plain', event.target.id);
    event.dataTransfer.effectAllowed = 'copy'; // 要素のコピーを許可する
});

// コピーを許可
copyBox.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy'; // 要素のコピーを許可する
});

copyBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    const droppedElement = document.getElementById(data);
    const newElement = document.createElement(droppedElement.tagName);
    newElement.textContent = droppedElement.textContent;
    newElement.style.margin = 0;
    event.target.appendChild(newElement);
});

// 移動を許可(ドラッグ要素はコピー限定なので動かない)
moveBox.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move'; // 要素の移動を許可する
});

moveBox.addEventListener('drop', (event) => {
    event.preventDefault();
    const data = event.dataTransfer.getData('text/plain');
    event.target.appendChild(document.getElementById(data));
});

サンプルコード effectAllowedとdropEffectの対応確認(drag-and-drop-sample/04.effect-allowed/app.js)
// ドラッグ要素(effectAllowedに none, copy, copyLink, copyMove, link, linkMove, move, all, uninitializedを設定)
const noneDrag = document.querySelector('#none-drag');
noneDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'none';
});

const copyDrag = document.querySelector('#copy-drag');
copyDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'copy';
});

const copylinkDrag = document.querySelector('#copylink-drag');
copylinkDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'copyLink';
});

const copymoveDrag = document.querySelector('#copymove-drag');
copymoveDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'copyMove';
});

const linkDrag = document.querySelector('#link-drag');
linkDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'link';
});

const linkmoveDrag = document.querySelector('#linkmove-drag');
linkmoveDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'linkMove';
});

const moveDrag = document.querySelector('#move-drag');
moveDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'move';
});

const allDrag = document.querySelector('#all-drag');
allDrag.addEventListener('dragstart', (event) => {
    event.dataTransfer.effectAllowed = 'all';
});

const uninitializedDrag = document.querySelector('#uninitialized-drag');
uninitializedDrag.addEventListener('dragstart', (event) => {
    // uninitializedはeffectAllowedの初期値
});

// ドロップ先(dropEffectにデフォルトのnone, none, copy, move, linkを設定)
const defaultDrop = document.querySelector('#default-drop');
defaultDrop.addEventListener('dragover', (event) => {
    event.preventDefault();
    // event.dataTransfer.dropEffectを設定しない
});

const noneDrop = document.querySelector('#none-drop');
noneDrop.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'none';
});


const copyDrop = document.querySelector('#copy-drop');
copyDrop.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
});


const moveDrop = document.querySelector('#move-drop');
moveDrop.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
});


const linkDrop = document.querySelector('#link-drop');
linkDrop.addEventListener('dragover', (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'link';
});


const dropList = document.getElementsByClassName('drop-area');
for (dropArea of dropList) {
    dropArea.addEventListener('drop', (event) => {
        event.preventDefault();
        event.target.style.color = 'red';
        setTimeout(() => event.target.style.color = '', 2000);
    });
}

😋 effectAllowedとdropEffectの組み合わせで設定します♪

フィードバッグ画像とカーソル

まずポイントをチェック

  • dragstartイベントでsetDragImage() メソッドでフィードバック画像を設定
  • CSSのcursorプロパティでマウスカーソルの種類を指定

ドラッグ時の見た目の設定をすることができます。フィードバック画像とはドラッグ時に薄く表示される画像のことです。またドラッグ時のマウスカーソルの見た目も設定することができます。分かりやすい見た目を提供することで、より直感的に理解しやすくなります。

フィードバック画像の設定

フィードバック画像はデフォルトで設定されていますが、独自の画像を指定することができます。setDragImageメソッドを使います。

setDragImageメソッド

  • 第一引数:画像に使用する要素(img要素など)
  • 第二引数:表示位置のx座標をpxで指定
  • 第三引数:表示位置のy座標をpxで指定

設定する画像は要素オブジェクトとして用意します。

const draggingImg = new Image();
draggingImg.src = './dragging.png';
star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setDragImage(draggingImg, 10, 10);
});

マウスカーソルの設定

ドラッグ&ドロップ関連のマウスカーソルをCSSで設定することができます。cursorプロパティにマウスカーソルの種類を示す値を指定します。

サンプルコードはgrabを初期値とし、要素にマウスが重なった時に手のカーソルに切り替わります。ドラッグ開始時にgrabbingを指定し、要素を掴んだ時に握り手のカーソルに切り替わります。ドラッグ終了時にはgrabに再設定しています。

サンプルコード抜粋(drag-and-drop-sample/05.visual-effect/index.html)
<h1 id="star" draggable="true" style="cursor: grab;"></h1>
サンプルコード全体像(drag-and-drop-sample/05.visual-effect/app.js)
const star = document.querySelector('#star');
const draggingImg = new Image();
draggingImg.src = './dragging.png';

star.addEventListener('dragstart', (event) => {
    event.dataTransfer.setDragImage(draggingImg, 10, 10);
    event.target.style.cursor = 'grabbing';
});

star.addEventListener('dragend', (event) => {
    event.target.style.cursor = 'grab';
});

😋 ドラッグ&ドロップ操作を視覚的にサポートできます♪

ファイルのドラッグ & ドロップ

まずポイントをチェック

  • OSからブラウザへファイルをドロップ可能
  • DataTransferオブジェクトのfilesプロパティから各ファイルを取得
    • getDataメソッドは使わない

ドラッグ&ドロップでOSからファイルを受け取ることができます。dropイベント時にDataTransferのfilesプロパティから取得します。filesプロパティにはドロップされたファイルの件数分のFileオブジェクトが入っています。取得は配列のように添え字を指定します。

サンプルコード全体像(drag-and-drop-sample/06.file-drop/app.js)

const box = document.querySelector('#box');

box.addEventListener('dragover', (event) => {
    event.preventDefault();
});

box.addEventListener('drop', (event) => {
    event.preventDefault();
    const file = event.dataTransfer.files[0];
    console.log(file);
});

😋 filesプロパティからファイルを受け取ります♪

おわりに

皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。

ドラッグ&ドロップAPIについて確認をしていただきました。
データの移動は各イベントで処理を行うように記述します。
ドラッグ&ドロップ操作の間に多数のイベントが発生するので、細かくユーザーにフィードバックを行えるなど様々な工夫の余地があります。

😋 これからもプログラミング学習頑張りましょう♪

参考リンク集(MDN Web Docs のリンク)
参考リンク集(web.dev のリンク)

HTML5 Drag and Drop APIの使用:https://web.dev/i18n/ja/drag-and-drop/

参考リンク集(その他 のリンク)
参考リンク集(サンプルコード)

drag-and-drop-sample(GitHub):https://github.com/ChanCode-Sample/JavaScript-API/tree/main/drag-and-drop-sample
Drag and Drop API を使った駐車場プロジェクト:https://park.glitch.me/
Drag and Drop API を使った駐車場プロジェクト(編集):https://glitch.com/edit/#!/park?path=script.js%3A1%3A0
Creating a Parking Game With the HTML Drag and Drop API:https://css-tricks.com/creating-a-parking-game-with-the-html-drag-and-drop-api/

Discussion