意外と知らないHTML5 API 第1回 Drag & Drop APIとは

Drag & Drop API実装の基礎を解説します。直接ファイルをドラッグ&ドロップしてアップロードしたり、ページ内の要素を入れ替えたりできるので、UIに寄与する機能が多いのが特徴です。

発行

著者 中島 直博 フロントエンド・エンジニア
意外と知らないHTML5 API シリーズの記事一覧

はじめに

Drag & Drop API*(以降、DnDと呼びます)はページ内の要素をドラッグで動かし、ほかの場所に移動させたり、ブラウザ外からファイルをページにドロップしたりといった操作を可能にするHTML5のAPIです。

*注:Drag & Drop API

HTML Living Standardの「6.7 Drag and drop」に記述されています。

DnDを使うことで、次のような機能を実装することができます。

  • 要素の順番や位置を入れ替える
  • ローカルにある画像やテキストをブラウザにドロップして読み込む

ドラッグ&ドロップを実現するためのライブラリとしてjQueryUIが有名ですが、モダンブラウザであれば、ライブラリを使わずにこれらの機能が実現できます。

DnDを使っている身近な例としては、SNSなどでファイルをアップロードする際のUIが挙げられます。フォームなどに直接ファイルをドラッグ&ドロップしてアップロードできます。

TwitterやFacebook、Dropbox、Gmailなどでも使われているので、利用したことがある方も多いのではないでしょうか。

ファイルをダイアログから選択するのと比べて、ドラッグ&ドロップでの操作は手間が少なく、ファイルを扱うUIとして馴染み深いです。

このシリーズでは2回に分けて、DnD APIの基本的な使い方と、実装する際の注意点やテクニックについて解説していきます。

APIの対応ブラウザ

HTML5のAPIの中でもDnDはかなり古くからあります。現在使われている主要なブラウザなら問題なく使えるでしょう。また、操作の性質上からスマートフォンではサポートされていません。

API Chrome Firefox Safari Opera IE
Drag and Drop API 4+ 3.5+ 3.1+ 12+ 5.5+

DnDでのイベント

DnDのイベントは大きく分けて2種類に分けられます。ドラッグする要素に使用するものとドロップを受け入れる要素に使用するものです。

イベント 発生するタイミング
dragstart ドラッグ開始時
drag ドラッグしている間
dragend ドラッグ終了時
dragenter ドラッグしている要素がドロップ領域に入ったとき
dragover ドラッグしている要素がドロップ領域にある間
dragleave ドラッグしている要素がドロップ領域から出たとき
drop ドラッグしている要素がドロップ領域にドロップされたとき

これらのイベントを使用してドラッグ&ドロップを実装していきます。まずは最低限の機能を備えたサンプルを見てみます。

実装の最小構成

赤いボックスを下のエリアにドラッグ&ドロップしてみてください。ドラッグしている要素がエリアに入ると、テキストがonDragOverとなり、ドロップされるとonDropになるのが確認できます。

ソースコード*は次のようになっています。

*注:JavaScriptのソースコード

ソースコードの見通しをよくするためにjQueryを使用しています。

...
<div class="box" draggable="true"></div>
<div id="dropzone" class="dropzone"></div>
...
...
$('#box').on('dragstart', onDragStart);
$('#dropzone').on('dragover', onDragOver);
$('#dropzone').on('drop', onDrop);

function onDragStart(e) {
  e.originalEvent.dataTransfer.setData('text', this.id);
}

function onDragOver(e) {
  e.preventDefault();
  this.textContent = 'onDragOver';
}

function onDrop(e) {
  e.preventDefault();
  this.textContent = 'onDrop';
}
...

単純な機能であれば、これだけのコードで、ドラッグ&ドロップを実現できます。ただし、DnDを使う上で最低限必要となるポイントがいくつかあります。

ドラッグしたい要素のdraggable属性にtrueを指定する

要素をドラッグ可能な状態にするには、ドラッグする要素のdraggable属性にtrueが指定されている必要があります。

例外としてimg要素と、href属性のついたa要素 はデフォルトでdraggable属性にtrueがセットされます*。

*注:draggable属性の仕様

7.8.5 The draggable attribute

ドラッグしたい要素のdragstartイベント内で何かしらのデータをセットする

DnDでのデータの受け渡しはDataTransferオブジェクト*を介して行います。DataTransferオブジェクトについては別の節で改めて解説します。

*注:DataTransfer

ChromeではDataTransferに値をセットしなくてもドラッグ&ドロップの操作を行えますが、FireFoxでは必ず値をセットする必要があります。値がセットされていない場合dragstartイベントが発生せず、処理が行えません。

ドロップを受け付ける要素のdragoverイベント内でpreventDefaultを実行する

ブラウザには何かがドロップされたときに行う動作がデフォルトで用意されています。例えば、ほかのウィンドウやローカルから画像をドロップすれば画像が開かれますし、リンクの設定された要素をドロップすればリンクのページに飛びます。

そのためdragoverイベントが起きたとき*、これらのデフォルトの動作を明示的に止め、その要素がdropイベントを受け付けることをブラウザに示さなければなりません。preventDefaultを行わないと、dropイベント自体が発生しません。

*注:dragoverイベント内でのpreventDefault

一部のブラウザではdragenterイベントでもpreventDefaultを実行する必要があります。

DataTransferオブジェクト

DataTransferオブジェクトは、ドラッグしている要素のデータを保持するために使います。DataTransferオブジェクトはDnDで発生するイベント内からのみアクセスすることができます。

DataTrasnferオブジェクトのメソッド

DataTransferオブジェクトにはデータの保持以外にも役割があります。

メソッド 役割
setData データセットする
getData セットされているデータを取得する
setDragImage ドラッグ中に表示されるイメージを変更する

setDatasetData('text', 'hogehoge')のように、第一引数にタイプを指定してデータをセットします。セットしたデータはgetData('text')とすることで取得できます。

setDragImageではドラッグ中に表示されるイメージを変更することができます。セットしない場合はドラッグしている要素自体が使われます。

setDragImageを使ったサンプルです。赤いボックスをドラッグすると別の画像が出てきます。

<div id="box" class="box" draggable="true"></div>
<div class="box blue" draggable="true"></div>
var dragImage = document.createElement('img');
dragImage.src = 'img/codegrid.png';

$('#box').on('dragstart', onDragStart);
function onDragStart(e) {
  e.originalEvent.dataTransfer.setData('text', this.id);
  e.originalEvent.dataTransfer.setDragImage(dragImage, 40, 40);
}

setDragImageにはDOMに存在しているimg要素やcanvas要素なども指定できます。setDragImageの第二、第三引数には指定した画像のどの部分にカーソルを置くかを、x座標とy座標の値で指定することができます。

DataTrasnferオブジェクトのプロパティ

プロパティはsetDataでセットされたデータに関する情報や、ドロップされたファイルの情報を持っています。

  • files:ドロップされたファイルの情報を保持。File APIのFileList形式で格納される。
  • typessetDataでセットされたデータの種類が格納されている。
  • dropEffect:設定した値が格納される。初期値はnone
  • effectAllowed:設定した値が格納される。初期値はall

dropEffectとeffectAllowedのプロパティ

dropEffectプロパティはドラッグ操作で許可したい操作の種類を示すことができます。effectAllowedプロパティはドロップ先で許可したい操作の種類を設定できます。dropEffectで設定した値がeffectAllowedに含まれている場合だけdropイベントが発生します。ドラッグ元の要素がどの種類の処理か(コピーや移動など)を期待しているのか、ドロップ先がどの種類の処理を受け付けるのかを明示することで望まない要素のドロップを避けることができます。

dropEffectのプロパティ

  • copy
  • move
  • link
  • none

effectAllowedのプロパティ

  • copy
  • move
  • link
  • copyLink
  • copyMove
  • linkMove
  • all
  • none
  • uninitialized

ただしここで設定したcopyなどの値が、ドロップ後の処理に直接影響を与えることはありません。dropEffectcopyを設定していたとしても、コピーの処理がされるわけではないことに注意してください。これらの処理に関しては、別途自分で実装する必要があります。

プロパティだけではわかりづらいと思いますので、dropEffecteffectAllowedを使ったサンプルを見ていきましょう。

3つのボックスにはeffectAllowedプロパティに、それぞれ異なる値が設定されています。4つのドロップエリアにはdropEffectプロパティに、それぞれ異なる値が設定されています。

ドラッグするボックスとドロップするエリアによって、カーソルに表示されるアイコンやドロップの可否などの挙動が違うことが確認できると思います。

...
<div id="box1" class="box copy" draggable="true"></div>
<div id="box2" class="box blue link" draggable="true"></div>
<div id="box3" class="box green all" draggable="true"></div>
<div id="dropzone1" class="dropzone small copy"></div>
<div id="dropzone2" class="dropzone small link"></div>
<div id="dropzone3" class="dropzone small none"></div>
<div id="dropzone4" class="dropzone small all"></div>
...
$('#box1').on('dragstart', dragCopy);
$('#box2').on('dragstart', dragLink);
$('#box3').on('dragstart', dragAll);

$('#dropzone1').on('dragover', effectMove);
$('#dropzone2').on('dragover', effectLink);
$('#dropzone3').on('dragover', effectNone);
$('#dropzone4').on('dragover', effectAll);
$('.dropzone').on('drop', onDrop);

// ドラッグする要素に許可するeffectを設定する
function dragCopy(e) {
  e.originalEvent.dataTransfer.setData('text', 'copy');
  e.originalEvent.dataTransfer.effectAllowed = 'copy';
}

function dragLink(e) {
  e.originalEvent.dataTransfer.setData('text', 'link');
  e.originalEvent.dataTransfer.effectAllowed = 'link';
}
function dragAll(e) {
  e.originalEvent.dataTransfer.setData('text', 'all');
  e.originalEvent.dataTransfer.effectAllowed = 'all';
}

// ドロップ先が受け入れるeffectを設定する
function effectMove(e) {
  e.preventDefault();
  this.textContent = 'dragover';
  e.originalEvent.dataTransfer.dropEffect = 'copy';
}

function effectLink(e) {
  e.preventDefault();
  this.textContent = 'dragover';
  e.originalEvent.dataTransfer.dropEffect = 'link';
}

function effectNone(e) {
  e.preventDefault();
  this.textContent = 'dragover';
  e.originalEvent.dataTransfer.dropEffect = 'none';
}

function effectAll(e) {
  e.preventDefault();
  this.textContent = 'dragover';
  e.originalEvent.dataTransfer.dropEffect = '';
}

function onDrop(e) {
  e.preventDefault();
  var type = e.originalEvent.dataTransfer.getData('text');
  this.textContent = 'onDrop: ' + type;
}
...

赤いボックスにはeffectAllowedcopyが設定されているので、ドロップできるのはdropEffectcopyが設定されているエリア、もしくは空の値が設定されたエリアのみです。

青いボックスにはeffectAllowedlinkが設定されているので、ドロップできるのはdropEffectlinkが設定されているエリア、もしくは空の値が設定されたエリアのみです。基本的な挙動は赤いボックスと同じです。

緑のボックスはdropEffectallが設定されています。allが設定された要素はdropEffectnoneが設定されていないすべてのエリアにドロップすることができます。dropEffectnoneを設定すると、すべてのドロップを受け付けないようになります。

ドロップできないエリアの上にボックスを動かすと、カーソルに表示されるドロップ可能のアイコンが消え、通常のポインタの状態になります。

この2つのプロパティを使うことで、ドラッグしている要素がどのような操作ができるかを視覚的に確認できるとともに、意図しない要素がドロップされるのを避ける助けをしてくれます。

適切なフィードバックを表示する

ドラッグ&ドロップという操作は直感的な反面、UIを見ただけでは、それがドラッグ可能であるかどうかが、わかりづらいという点があります。ユーザーが気づくのを助けるためにも各操作ごとにフィードバックを表示してあげる必要があります。

  • ドラッグできる要素にカーソルが載ったときにドラッグ可能であることを示す
  • ドラッグが開始されたときドロップできるゾーンを示す
  • ドロップエリアの上に要素が載ったときにドロップ可能かどうか示す

このようなポイントでフィードバックを表示すると、ユーザーにドラッグ&ドロップでの操作が可能であることを知らせる助けになります。フィードパックを表示するサンプルを見てみましょう。

...
<div id="box" class="box" draggable="true"></div>
<div id="dropzone" class="dropzone"></div>
...
...
$('#box')
  .on('dragstart', onDragStart)
  .on('dragend', onDragEnd);

var $dropzone = $('#dropzone')
    .on('dragover', onDragOver)
    .on('dragenter', onDragEnter)
    .on('dragleave', onDragLeave)
    .on('drop', onDrop);

function onDragStart(e) {
  e.originalEvent.dataTransfer.setData('text', e.originalEvent.target.id);
  addDraggingEffect();
}

function onDragEnter(e) {
  addEnterEffect();
}

function onDragLeave(e) {
  removeEnterEffect();
}

function onDragOver(e) {
  e.preventDefault();
}

function onDragEnd(e) {
  resetAllEffect();
}

function onDrop(e) {
  e.preventDefault();
  var text = e.originalEvent.dataTransfer.getData('text');
  this.textContent = text + 'がドロップされました';
}

// フィードバックの表示を制御するメソッド
function addDraggingEffect() {
  $dropzone.addClass('dragging');
}
function removeDraggingEffect() {
  $dropzone.removeClass('dragging');
}
function addEnterEffect() {
  $dropzone.addClass('dragenter');
}
function removeEnterEffect() {
  $dropzone.removeClass('dragenter');
}
function resetAllEffect(e) {
  removeDraggingEffect();
  removeEnterEffect();
}
...

dragstartイベントの発生タイミングで、ドロップエリアのスタイルを変更することでドロップ可能であることを示します。dragenterイベントとdragleaveイベントではそれぞれ、ドラッグしている要素がドロップ可能な領域に出入りしていることがわかるようにスタイルを変更しています。

dragleaveイベントはdragendイベントが発生した場合には発生しないので、変更したスタイルをリセットする処理を記述するにはdragendイベント内が適しています。dragendイベントはdragstartイベントが発生した場合には必ず発生します。

...
.dropzone {
border: 2px solid #555;
margin-top: 5px;
width: 246px;
height: 100px;
position: relative;
font-size: 12px;
}
.dragging {
color: rgba(0,0,0, .4);
background-color: rgba(0,0,255, .1);
border-color: rgba(0,0,255, .3);
}
.dragging:after {
content: "ここにドロップできます";
text-align: center;
position: absolute;
left: 0;
width: 100%;
line-height: 100px;
}
.dragenter {
border-color: rgba(255,0,0, .3);
background-color: rgba(255,0,0, .1);
}

各タイミングのフィードバックの表示はCSSを使ってclass属性の切り替えで行うのが、パフォーマンスの面からも適しています。

注意したい点として、dragイベントはドラッグしている間、dragoverイベントはドラッグしている要素が上に載っている間、ずっと発生し続けるので、ここに何かしらの処理を書くのはオススメしません。もし処理を書く必要があるのなら、throttledebounceを使って、処理を間引くとよいでしょう。

まとめ

今回は簡単なサンプルを例にDrag and Drop APIの基本的な使い方と、フィードバックの表示について解説しました。

次回は、ブラウザ外からのドラッグ&ドロップに対する処理、細かいフィードバックの設定、ファイルがドロップされた際の処理などについて解説する予定です。