uhyohyo.net

JavaScript初級者から中級者になろう

十二章第七回 Drag and Drop API 2

このページの最終更新日:

今回は、前回に引き続きDrag and Drop API (DnD API)を扱います。前回はもっぱらページ内で要素をドラッグ&ドロップする場合を扱いましたが、実はページの外から何かがドラッグされてくるという場合もあります。DnD APIはそのような場合にも対応できます。

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

File APIの回でファイルを読み込むためにinput要素を使用しましたね。実は、ファイルをJavaScriptから読み込む方法はもう1つあります。それが、ブラウザ内へファイルをドラッグ&ドロップしてもらうことです。

実は、ページの外からファイルがドラッグ&ドロップされてきた場合も、ドロップ先の要素ではdragenter、dragoverその他のイベントが発生します。

ファイルがどこからかドラッグされてきた場合、やはりdataTransferに情報が入っています。実は、dataTransferはfilesというプロパティを持っており、これはFileListオブジェクトです。当然、入っているのはドラッグされているファイルの情報ですので、これを用いてファイルを取得することができます。


function drop(ev){
  //ファイルが複数かもしれないのでforループで調べる
  for(var i=0;i<ev.dataTransfer.files.length;i++){
    var file=ev.dataTransfer.files[i];

    //新しいp要素を作って表示
    var p=document.createElement("p");
    p.textContent= file.name;
    ev.target.appendChild(p);
  }
}

今までの説明を総合するとこんな感じになります。サンプルで実際に確かめましょう。

ちなみに、紹介しそびれたdataTransferのメソッドで、clearDataというものがあります。これはフォーマット文字列を引数にとって呼び出すことで、そのフォーマットのデータをdataTransferから削除できます。

DataTransferを扱う新しい方法

実は、今まで紹介してきたDnD APIのうち、dataTransferを扱うメソッド(setData, getData, clearData)は実はちょっと古い方法で、最新の仕様では新しい方法が用意されています(ただし、古い方法も使用可能であると定められており、実際に多く利用されているので古い方を使ってもすぐに問題になるわけではありません)。

この記事を最初に書いたとき(2014年7月)は新しい方法をサポートするブラウザが全然ありませんでしたのでこの講座では古い方法を先に紹介しています。というか、現在(2019年7月)でもSafariというブラウザが未だに未対応となっています。そのため、以下の内容は今すぐ実用できないかもしれません。とはいえ、古い方法だけ紹介するのもおかしな話なので新しい方法もしっかり解説します。

新しい方法では、DataTransfer内のデータはdataTransferが持つitemsプロパティに集約されます。これにはDataTransferItemListオブジェクトが入っており、このオブジェクトを通してデータをやりとりします。

データをdataTransferに格納するには、このDataTransferItemListオブジェクトのaddメソッドを使います。setDataメソッドとは逆で、第一引数にデータ文字列、第二引数にフォーマット文字列となっています。

この新しい方法の特徴は、addメソッドの第1引数に別の方法で生成したFileオブジェクトを渡すことでdataTransferにファイルを追加することができる点です。古い方法では、ブラウザウィンドウの外からファイルをドラッグしてきた場合しかDnD APIでファイルを扱う機会はありませんでしたが、新しい方法ではこのようなパターンもあるのです。

そして、入っているデータを得る方法ですが、実はこのDataTransferItemListオブジェクトは例によってlengthプロパティを持ちます。これだけでピンとくるかもしれません。dataTransfer.items[0]のように、添字で入っているデータを取得できます。

そのため、ループして目的のフォーマットのものを探す必要があります。getDataメソッドを使った場合は一発で取得できたのでちょっと面倒ですね。

しかも、dataTransfer.items[0]のように取得したデータは生のデータではありません。ここで得られるのはDataTransferItemオブジェクトというもので、このオブジェクトからさらにデータを引き出すのが少し面倒です。

DataTransferItemオブジェクトはkindプロパティとtypeプロパティを持ち、kindプロパティは"string""file"のどちらかです。つまり、このデータがテキストなのかファイルなのかを示しています。typeプロパティはフォーマット文字列です。

そして、文字列の場合はgetAsStringメソッドで、ファイルの場合はgetAsFileで生のデータを取得します。getAsFileの場合は引数なしで、返り値でFileオブジェクトが得られるのですが、getAsStringメソッドの場合はさらに面倒です。このメソッドには引数でコールバック関数を渡す必要があり、その関数にデータが渡されます。

この方法は面倒ですが、dataTransferに入っているデータを列挙したい場合などはこの方法を使う必要があります。特に、この方法によって中にどのようなデータ(データのフォーマットや種類)が入っているかを知ることができます。前回dragstartイベントとdropイベント以外ではdataTransfer内のデータを見ることができないと解説しましたが、実は実際のデータを見ることはできないもののデータの種類などの情報は見ることができます。これにより、例えばdragenterイベントで、データの種類によって受け入れるかどうかを決めるというような挙動が可能になります。

なお、他にもDataTransferItemListはremoveメソッド(除去するデータを番号で指定)やclearメソッド(引数なしで全て削除)を持ちます。

また、dataTransfer.items.adddataTransfer.setDataにはひとつ違いがあります。既に存在するタイプのデータを追加しようとしたとき、前者はエラーとなるのに対し後者は上書きします。

これらを踏まえて前回のサンプル3を書き換えたのがサンプル2です。特にデータの取得がごちゃごちゃしていますね。さっき述べたことを踏まえて、dragenter時点でデータの種類を表示するようにしてみました。div要素の他に外からファイルをドラッグしてきたりしても面白いでしょう。

その他のメソッドなど

さて、前回と今回の説明でやりたいことはだいたいできるでしょう。ここでは補足説明を加えます。以下の説明は古い方法も新しい方法も共通です。

実は、DataTransferには、setDragImageというメソッドがあり、ドラッグ中に表示する画像を設定できます。例えばdragstartでこれを呼び出すと、ドラッグ中のマウス等の画像表示を変更できるでしょう。使い方は以下の通りです。


dataTransfer.setDragImage( element, x,y);

elementというのは、画像として使用する要素です。一般的にはimg要素ですが、他にも任意の要素を指定可能です。x,yは数値で指定します。これは画像のつかむ場所を示す座標です。

最後に、DataTransferのeffectAllowedプロパティを紹介します。これは、ドラッグオペレーション(copy・link・move)のうちどれが可能かを示すものです。

ドラッグオペレーションはdragoverイベント時にdataTransfer.dropEffectプロパティを指定することで設定できますが、このとき設定できる値はeffectAllowedにより制限されます。

effectAllowedはdragstartイベント時しか書き込むことができません。これにより、ドラッグされている側がどのようなドラッグオペレーションが許されるのか制御する狙いです。

一方dropEffectというのは、ドロップ先(受け入れ先)が指定するドラッグオペレーションを示します。もしこれらが咬み合わない場合(effectAllowedで許されていないドラッグオペレーションがdropEffectで指定された場合)は"none"扱いとなりドロップできません。

effectAllowedには以下の値を設定することができます。

none
ドラッグできません。ドラッグオペレーションは必ずnoneになります。
copy
copyのみ許されます。
copyLink
copyとlinkが許されます。
all
3種類全てが許されます。
link
linkのみ許されます。
linkMove
linkとmoveが許されます。
move
moveのみ許されます。
uninitialized
デフォルト値です。3種類全てが許されます。

そして、effectAllowedにはもう1つ意味があります。上のリストをよく見てみると、allとuninitializedが同じに見えます。実は、dragenterイベント及びdragoverイベントにおいては、あらかじめdataTransfer.dropEffectプロパティが自動的にセットされています。これはeffectAllowedに基づいて決められます。

例えば、effectAllowedが"copy"の場合はdropEffectは自動的に"copy"にセットされています。そのため、effectAllowedを適切に設定してやればdragoverでわざわざdropEffectを手動で設定する必要がありません。ただし、dragoverをキャンセルするのは行う必要があります。

また、複数のところから異なるeffectAllowedを持つものがドラッグされてくるがこちら側では1種類または2種類のドラッグオペレーションしか受け付けないという場合には、手動で設定したりdropEffectの値を見たりしてやる必要があるでしょう。

ちなみに、effectAllowedが"copyMove"のように複数の値が許されている場合は、ブラウザがいい感じに決めてくれます。例えばWindowsでは、普通にドラッグすると移動(move)だけどCtrlキーを押しながら移動するとコピー(copy)になります。気が利くブラウザならばそういった情報を与えてくれるでしょう。しかし、あまり期待するべきではありません。

結局uninitializedとallは何が違うかというと、微妙な違いではありますが、uninitializedの場合は何をドラッグしているかによってさらに気を利かせてくれます。

全2回と長かったですが、以上でDrag and Drop APIの説明を終わります。もしドラッグ&ドロップという動作が必要になったら必ず関わってくるAPIなのでぜひ自分のものにしておきましょう、