JavaScriptを使いWeb上でファイルのドラッグ&ドロップを実現する例
HTML5になってからWeb要素へのドラッグ&ドロップがサポートされ、今となってはありふれた機能でサンプルコードもそこら中に公開されていますが、少し気になったことがあったので備忘録として記事に残しておこうと思いました。
ドラッグ&ドロップでファイルをアップロードする動作サンプル
何はともあれ、まずは動くサンプルです。
「ここにファイルをドロップ」というエリアにファイルをドラッグ&ドロップすると、そのファイルの名前、MIMEタイプ、更新日などが表示されます。
あくまでINPUT要素へのファイルのセットだけで実際どこかにファイルをアップロードするわけではないのでご安心ください。
ここにファイルをドロップ
解説
HTMLとCSS部分の解説
<p id="drop_area">ここにファイルをドロップ</p>
<input type="file" id="userfile">
<div id="file_info"></div>
<style>
#drop_area {
line-height:200px;
width:200px;
text-align:center;
border:1px dotted #808080;
color:#808080;
}
</style>
特にこれといって珍しいことはしていないと思いますが、
- ドロップエリアであるPタグ
- ファイルを選択する要素であるINPUTタグ
- ファイル情報を表示するためのDIVタグ
で構成されています。
ちなみにCSS部分でline-height:200px;としているのは1行を200pxにすることで文字を縦の中央に配置するためですね。
横の中央寄せはtext-align:center;で一発ですが、縦の中央寄せは意外とややこしくて、必ず1行と決まっているならline-heightで指定するのが一番楽なのでそうしているだけです。
JavaScript部分の解説
<script>
document.getElementById('drop_area').addEventListener('dragover', function () {
event.preventDefault();
this.style.backgroundColor = '#80ff80';
});
document.getElementById('drop_area').addEventListener('dragleave', function () {
this.style.backgroundColor = '';
});
document.getElementById('drop_area').addEventListener('drop', function () {
event.preventDefault();
this.style.backgroundColor = '';
if (event.dataTransfer.files.length > 0) {
document.getElementById('userfile').files = event.dataTransfer.files;
document.getElementById('userfile').dispatchEvent(new Event('change'));
}
});
document.getElementById('userfile').addEventListener('change', function () {
document.getElementById('file_info').innerHTML =
'ファイル名:'+this.files[0].name+'<br>'+
'タイプ:'+this.files[0].type+'<br>'+
'サイズ:'+this.files[0].size+'<br>'+
'更新日:'+this.files[0].lastModifiedDate+'<br>';
});
</script>
ドロップエリアのIDはdrop_areaなので、その要素に対して dragover / dragleave / drop の3つのイベントを追加しています。
ondragover
dragoverはファイルがドラッグされて、要素の上に重なったときに発生するイベント。
ここではまず、event.preventDefault();でデフォルトの動作を中止しています。ここでいうデフォルトの動作とはブラウザがファイルをドラッグされたときにする動作のことですね。
preventDefaultを実行しないとブラウザが直接ドラッグされたファイルを開いちゃう。
そして、ここにファイルをドロップして良いんだよ~ということをユーザーにわかりやすくするために
this.style.backgroundColor = '#80ff80';
というように背景色を緑にしています。
ondragleave
これはドラッグしていたファイルが要素から離れた場合に呼び出されるイベントなので、単に背景色を元に戻しているだけです。
ondrop
このdropイベントが本命ですが、本命のわりにほんの数行で終わり。
preventDefaultは先述したdragoverのときと同じですし、backgroundColorをクリアしたのもdragleaveのときと同様。実質、ここでやっているのは event.dataTransfer.files をINPUT要素である userfile にコピーしているだけ。
document.getElementById('userfile').files = event.dataTransfer.files;
この部分です。
簡単でしょう?
addEventListenerで追加したイベントはdispatchEventで発火する
ファイルのドラッグ&ドロップの話からは少し横道に逸れるのですが、イベントの登録方法って複数あるじゃないですか。
↓このパターンと、
document.getElementById('userfile').onchange = function () { alert('ファイルが変更されたよ!'); };
↓このパターン
document.getElementById('userfile').addEventListener('change', function () { alert('ファイルが変更されたよ!'); });
先述のドラッグ&ドロップの例ではドロップしたときだけではなく、ファイル選択ボタンで選んだときにもファイル情報を表示したかったため、どちらの場合でもonchangeイベントを通ってほしいわけですが、単に document.getElementById('userfile').files = event.dataTransfer.files; を実行しただけではonchangeイベントは発生しません。
そのため明示的にonchangeを実行しなければならないのですが、イベントの登録方法によって呼び出し方が異なります。
document.getElementById('userfile').onchange = function (); でイベントを追加した場合は document.getElementById('userfile').onchange(); でイベント発火できますが、addEventListenerの場合はdispatchEventを使わないといけません。
イベント登録方法についてふんわりと何となくで記憶している人はたまにここでハマるんですよね。onchange();実行してもなぜかエラーになる!と。
まぁ複雑なことをしないなら、onchange = function () { } でイベント登録しときゃ良いとも言えるのですがw ひとつのイベントに複数の関数を登録したいケースも出てくると思うので、addEventListenerを使うケースもあるでしょう。
いつか書きたいと思いつつ、これ単独で書くほどの内容でもないなーと思っていたので、今回の記事に割り込ませてみました。
fileはRead Onlyではない
さて、ファイルのドラッグ&ドロップの話に戻ります。
<input type="file">のファイル要素についてしばしばRead Only(読み取り専用)と言い切るような解説が見受けられます。
まぁ document.getElementById('userfile').value = 'c:\Documents\password.txt'; みたいに勝手にローカルファイルセットされて読み取られたらえらいこっちゃなので、読み取り専用と言えなくもなくもないのですが、それだと誤解しちゃう人いると思うんですよ。
例えばレンタルブログ等の画像アップロード画面で、画像を複数選択した後、Web上で「あ、やっぱこのファイルとこのファイルはアップしなくていいや」とバツボタンを押して消したりするUI見たことないですか? あるいはファイルの並び順を変えたり、1個1個ファイルを選択して最後にアップロードするようなUI。
もしもinput要素が完全にRead Onlyならそんなことできませんよね。
file要素をコピーする具体例
[下にコピーする]ボタンを押すとINPUT要素をコピーするサンプルです。
何も選択せずに[下にコピーする]ボタンを押しても何も起こりませんが、最初の[ファイルを選択]ボタンで何らかのファイルを選んだ後に[下にコピーする]ボタンを押すと、下にある4つの[ファイルを選択]ボタンにもファイルがセットされたのが確認できるはずです。
サンプルコード
<p><input type="file" id="userfile1">
<button id="copy_input">下にコピーする</button></p>
<p><input type="file" id="userfile2">
<input type="file" id="userfile3">
<input type="file" id="userfile4">
<input type="file" id="userfile5"></p>
<script>
document.getElementById('copy_input').addEventListener('click', function () {
document.getElementById('userfile2').files = document.getElementById('userfile1').files;
document.getElementById('userfile3').files = document.getElementById('userfile1').files;
document.getElementById('userfile4').files = document.getElementById('userfile1').files;
document.getElementById('userfile5').files = document.getElementById('userfile1').files;
});
</script>
file要素はコピーも上書きも可能
つまり何が言いたいかというと、file要素へ好き勝手にローカルファイルのパスをセットするようなことは出来ませんが、一度ユーザーアクションによって選ばれたファイル要素を上書きすることは可能、ということです。
これが可能でなければ昨今のクラウドストレージサービスのようなUIは成り立ちません。
まとめ
ファイルのドラッグ&ドロップの実装の話から、どちらかというとfile要素のコピーに関しての話がメインになりましたが、それもそのはず、ぼくが実際この部分でドハマリしたからですw
業務向けのWebアプリ開発に携わってきましたが、その中には社内用のCMS(Contents Management System)、まぁブログみたいなものですね。そういうシステムの開発もありました。そのシステムでは画像ファイルのアップロードにWeb UIを使うわけですが、このときアップロードしてから並び順を変えたり、削除したり、追加したり、なんてしていると負荷が高かったため、なんとかWebブラウザ上で先に並び替えや追加/削除などをして、ひととおり配置が決まってからアップロードしたかったんですね。
そのとき、file要素は上書きできない云々の記述を読んで頭を抱えていたのですが、実際やってみたらできるじゃん!!とw
そんなわけで、file要素の扱いに悩んでいてこの記事にやってくる人がどのくらいいるかわかりませんが、備忘録として残しておこうと思いました。