意外と知らないHTML5 API 第1回 Drag & Drop APIとは
Drag & Drop API実装の基礎を解説します。直接ファイルをドラッグ&ドロップしてアップロードしたり、ページ内の要素を入れ替えたりできるので、UIに寄与する機能が多いのが特徴です。
- カテゴリー
- JavaScript >
- ブラウザ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属性の仕様
ドラッグしたい要素の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 | ドラッグ中に表示されるイメージを変更する |
setData
はsetData('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形式で格納される。
- types:
setData
でセットされたデータの種類が格納されている。 - 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などの値が、ドロップ後の処理に直接影響を与えることはありません。dropEffect
にcopy
を設定していたとしても、コピーの処理がされるわけではないことに注意してください。これらの処理に関しては、別途自分で実装する必要があります。
プロパティだけではわかりづらいと思いますので、dropEffect
とeffectAllowed
を使ったサンプルを見ていきましょう。
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;
}
...
赤いボックスにはeffectAllowed
にcopy
が設定されているので、ドロップできるのはdropEffect
にcopy
が設定されているエリア、もしくは空の値が設定されたエリアのみです。
青いボックスにはeffectAllowed
にlink
が設定されているので、ドロップできるのはdropEffect
にlink
が設定されているエリア、もしくは空の値が設定されたエリアのみです。基本的な挙動は赤いボックスと同じです。
緑のボックスはdropEffect
にall
が設定されています。all
が設定された要素はdropEffect
にnone
が設定されていないすべてのエリアにドロップすることができます。dropEffect
にnone
を設定すると、すべてのドロップを受け付けないようになります。
ドロップできないエリアの上にボックスを動かすと、カーソルに表示されるドロップ可能のアイコンが消え、通常のポインタの状態になります。
この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
イベントはドラッグしている要素が上に載っている間、ずっと発生し続けるので、ここに何かしらの処理を書くのはオススメしません。もし処理を書く必要があるのなら、throttle
やdebounce
を使って、処理を間引くとよいでしょう。
まとめ
今回は簡単なサンプルを例にDrag and Drop APIの基本的な使い方と、フィードバックの表示について解説しました。
次回は、ブラウザ外からのドラッグ&ドロップに対する処理、細かいフィードバックの設定、ファイルがドロップされた際の処理などについて解説する予定です。