HTML5 から DOM に追加された File API によって、Web ページがユーザに自身の環境下のファイルを要求し、その内容を読み込めるようになりました。ファイルの選択は HTML の <input>
要素もしくはドラッグ&ドロップから行えます。
File API を拡張や他の chrome コードから利用することもできます。この場合、もういくつか知っておきたい機能があります。詳細は Using the DOM File API in chrome code をご覧下さい。
HTML からファイルを選択する
File API を使ってなにか1つファイルを選択してみましょう。
<input type="file" id="input" onchange="handleFiles(this.files)">
ユーザがファイルを選択すると、handleFiles()
関数が呼び出されます。関数には FileList
オブジェクトが与えられますが、このオブジェクトにはユーザが選択したファイルを表す File
オブジェクトが格納されています。
複数のファイルを選択させたいときはただ、input
要素に multiple
属性を追加すればよいだけです。
<input type="file" id="input" multiple onchange="handleFiles(this.files)">
この場合、handleFiles()
関数に渡されるファイルリストには、ユーザが選択した各ファイルに対応する File
オブジェクトが格納されています。
click() メソッドを使い input 要素を隠す
Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) から、ファイル選択用の <input>
要素を隠し、あなた好みの見た目にしたファイル選択用インターフェースを用意することができるようになりました。input
要素のスタイルを display: none
とし、その上で click()
メソッドを呼び出せばよいのです。
次のような HTML を考えてみましょう。
<input type="file" id="fileElem" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)">
<a href="#" id="fileSelect">ファイルを選択</a>
これに次のようなスクリプトを書けば、リンクをクリックしてファイルピッカーを呼び出せます。
var fileSelect = document.getElementById("fileSelect"), fileElem = document.getElementById("fileElem"); fileSelect.addEventListener("click", function (e) { if (fileElem) { fileElem.click(); } e.preventDefault(); // "#" に移動するのを防ぐ }, false);
もちろん、リンクではなくボタンなどにして、スタイルを思うようにカスタマイズできます。
動的に change イベントリスナを登録する
もし input
要素が jQuery のような JavaScript ライブラリによって生成されている場合は、 element.addEventListener()
を使用し change
イベントリスナを登録する必要があります。
var inputElement = document.getElementById("inputField"); inputElement.addEventListener("change", handleFiles, false); function handleFiles() { var fileList = this.files; /* ファイルリストを処理するコードがここに入る */ }
この場合、handleFiles()
関数は引数を取るのではなく、ファイルリストを探すことに注意してください。このようにして登録されたイベントリスナには、引数を与えられないのです。
オブジェクト URL を利用する
Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1) では、 window.URL.createObjectURL()
メソッドと window.URL.revokeObjectURL()
メソッドがサポートされました。これらのメソッドを使えば、 File
オブジェクトをシンプルな URL として参照できます。ユーザのコンピュータにあるファイルも例外ではありません。
HTML ファイルから URL で参照したい File
オブジェクトがある場合、そのオブジェクト URL は次のように作成できます。
var objectURL = window.URL.createObjectURL(fileObj);
オブジェクト URL は File
オブジェクトを識別する文字列です。window.URL.createObjectURL()
メソッドを呼び出すたびに、一意なオブジェクト URL が生成されます。これはたとえ既に同じファイルについてオブジェクト URL を生成していたとしてもです。
オブジェクト URL はドキュメントが解放された際に自動的に解放されますが、もしあなたのページが動的にオブジェクト URL を扱う場合は、window.URL.revokeObjectURL()
メソッドを使い明示的に開放する方がよいでしょう。
window.URL.revokeObjectURL(objectURL);
ドラッグ&ドロップでファイルを選択する
ドラッグ&ドロップでファイルを読みこませることもできます。
これを実装するにあたって最初にすることは、ドロップ領域の生成です。どの部分がファイルのドロップを受け付けるかは Web アプリケーションのデザインによりますが、ある要素がドロップイベントを受け付けるようにするのは簡単です。
var dropbox; dropbox = document.getElementById("dropbox"); dropbox.addEventListener("dragenter", dragenter, false); dropbox.addEventListener("dragover", dragover, false); dropbox.addEventListener("drop", drop, false);
この例では、id
に dropbox
を持つ要素をドロップ領域としました。要素をドロップ領域にするには、dragenter
, dragover
, drop
イベントのリスナを登録すればよいのです。
この例で dragenter
, dragover
イベントについて何かする必要はとくにありません。ですので渡す関数はとてもシンプルです。ただイベントの伝搬を停止し、規定のアクションが起こらないようにしているだけです。
function dragenter(e) { e.stopPropagation(); e.preventDefault(); } function dragover(e) { e.stopPropagation(); e.preventDefault(); }
重要なのは drop()
関数です。
function drop(e) { e.stopPropagation(); e.preventDefault(); var dt = e.dataTransfer; var files = dt.files; handleFiles(files); }
ここでイベントから dataTransfer
フィールドを受け取り、そこからファイルリストを取得して handleFiles()
に渡しています。ここからのファイル操作は input
要素を使うのとまったく同じです。
ファイルの情報を得る
FileList
オブジェクトはユーザが選択したファイルを格納しています。それぞれのファイルは File
オブジェクトとして表現されています。ユーザがいくつファイルを選択したかは、ファイルリストの length
プロパティから取得できます。
var numFiles = files.length;
個々の File
オブジェクトは、ファイルリストに配列のようにアクセスすることで取得できます。
for (var i = 0, numFiles = files.length; i < numFiles; i++) { var file = files[i]; ... }
このループはファイルリスト中のすべてのファイルにアクセスします。
File
オブジェクトには3つのプロパティが用意されており、ファイルに関する有益な情報を得られます。
name
- ファイル名 (読み取り専用)。このプロパティはファイル名のみを持ち、パスに関する情報は何も持ちあわせていません。
size
- ファイルサイズ (読み取り専用)。64ビット整数のバイトで表現されています。
type
- ファイルの MIME タイプ (読み取り専用)。MIME タイプが決定できないときは空文字列 (
""
) を返します。
例: ファイルサイズを表示
次のコードは size
プロパティを利用する例です。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>File(s) size</title> <script> function updateSize() { var nBytes = 0, oFiles = document.getElementById("uploadInput").files, nFiles = oFiles.length; for (var nFileId = 0; nFileId < nFiles; nFileId++) { nBytes += oFiles[nFileId].size; } var sOutput = nBytes + " bytes"; // optional code for multiples approximation for (var aMultiples = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"], nMultiple = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, nMultiple++) { sOutput = nApprox.toFixed(3) + " " + aMultiples[nMultiple] + " (" + nBytes + " bytes)"; } // end of optional code document.getElementById("fileNum").innerHTML = nFiles; document.getElementById("fileSize").innerHTML = sOutput; } </script> </head> <body onload="updateSize();"> <form name="uploadForm"> <p><input id="uploadInput" type="file" name="myFiles" onchange="updateSize();" multiple> selected files: <span id="fileNum">0</span>; total size: <span id="fileSize">0</span></p> <p><input type="submit" value="Send file"></p> </form> </body> </html>
例: ユーザが選択した画像のサムネイルを表示
あなたはフォト共有サイトをつくっており、ユーザがアップロードする写真を選んでいるときに、そのサムネイル一覧をアップロードすることなしにプレビューさせたいとします。これを実現するのも簡単で、input
要素もしくはドロップ領域を用意してから、次のような handleFiles()
関数を呼べば良いのです。
function handleFiles(files) { for (var i = 0; i < files.length; i++) { var file = files[i]; var imageType = /image.*/; if (!file.type.match(imageType)) { continue; } var img = document.createElement("img"); img.classList.add("obj"); img.file = file; preview.appendChild(img); var reader = new FileReader(); reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; }; })(img); reader.readAsDataURL(file); } }
ユーザが選択したファイルを処理するループで、ファイルの type
属性を見てそれが画像なのかを確かめています (正規表現で /image.*/
にマッチするかを調べています)。画像だと分かったファイルについて新しい img
要素を生成します。画像のサイズを指定したり、ボーダーをつけたり、ドロップシャドウを与えたりするのには CSS を使えばよいので、このコードでする必要は特にありません。
img
要素には obj
という class
が追加され、DOM ツリーから探しやすくなります。また、file
という属性を設け、そこに画像の File
オブジェクトを指定します。これにより、あとで実際にアップロードする画像を保持しておけるのです。最後に、 appendChild()
で新しいサムネイルを文書のプレビュー領域に追加します。
そして、画像を FileReader
で非同期に読み込み、img
要素に紐付けます。新しい FileReader
オブジェクトを生成し、onload
関数をセットアップします。そして、readAsDataURL()
を呼び、ファイル読み込みをバックグラウンドで開始します。画像ファイルの内容がすべて読み込まれると、それらは data:
URL に変換され、onload
のコールバックに渡されます。ここでの実装はただ img
要素の src
属性に読み込まれた画像をセットしているだけです。これでユーザの画面上にサムネイルを表示させられます。
例: オブジェクト URL で画像を表示
この例ではオブジェクト URL を使って画像のサムネイルを表示します。さらに、ファイル名やファイルサイズも表示します。サンプルもあります。
インターフェースとなる HTML は次のようになりました。
<input type="file" id="fileElem" multiple accept="image/*" style="display:none" onchange="handleFiles(this.files)"> <a href="#" id="fileSelect">Select some files</a> <div id="fileList"> <p>No files selected!</p> </div>
まずファイルを読み出す input
要素をつくり、ファイルピッカーを呼び出すリンクもつくります。あまり見栄えのよくない input
要素の UI を隠したいからです。これは先のセクションで紹介したものと同じです。最後に、サムネイルを表示する領域を <div>
要素で作ります。各要素の id
属性はそれぞれ fileElem
, fileSelect
, fileList
としました。
handleFiles()
メソッドはこんな風になります。
window.URL = window.URL || window.webkitURL; var fileSelect = document.getElementById("fileSelect"), fileElem = document.getElementById("fileElem"), fileList = document.getElementById("fileList"); fileSelect.addEventListener("click", function (e) { if (fileElem) { fileElem.click(); } e.preventDefault(); // prevent navigation to "#" }, false); function handleFiles(files) { if (!files.length) { fileList.innerHTML = "<p>No files selected!</p>"; } else { var list = document.createElement("ul"); for (var i = 0; i < files.length; i++) { var li = document.createElement("li"); list.appendChild(li); var img = document.createElement("img"); img.src = window.URL.createObjectURL(files[i]); img.height = 60; img.onload = function(e) { window.URL.revokeObjectURL(this.src); } li.appendChild(img); var info = document.createElement("span"); info.innerHTML = files[i].name + ": " + files[i].size + " bytes"; li.appendChild(info); } fileList.appendChild(list); } }
まず、前述した3つの要素を取得します。
ファイルが選択されたときに、handleFiles()
に渡された FileList
が null
だった場合、"No files selected!" というテキストを書き込んでいます。null
でない場合は、次のステップに従い、fileList
内にサムネイルを書き込みます。
- 新しく
<ul>
要素を作成する -
element.appendChild()
メソッドを使用し、そのul
要素をfileList
に追加する -
FileList
オブジェクト (files
) 中の各File
オブジェクトについて以下を実行する- 新しく
<li>
要素を生成し、さきほどのul
要素に追加する - 新しく
<img>
要素を生成する -
window.URL.createObjectURL()
でファイルのオブジェクト URL を作成し、img
要素のsrc
属性に指定する - 画像の高さを60ピクセルに指定する
- 画像の
load
イベントハンドラを追加し、画像が読み込まれたらwindow.URL.revokeObjectURL()
にimg.src
で与えたオブジェクトを渡し、オブジェクト URL を解放するようにする (画像が読み込まれたら必要なくなるため) li
要素をul
要素に追加する
- 新しく
例: ユーザが選択したファイルを送信
ひとつ前の画像サムネイルの例のように、ユーザが選択したファイルをサーバへ送信したい場合もあるでしょう。これもとても簡単に、そして非同期に行うことができます。
アップロードタスクの生成
では、さきほどのサムネイルを生成する例を拡張しましょう。さきほどの例では、サムネイル画像には obj
という class
がつけられており、またそれぞれの File
オブジェクトは file
という属性につけられていました。これにより、ユーザがアップロードのため選択した画像を得るのはとても簡単です。 document.querySelectorAll()
を使うと、次のように書けます。
function sendFiles() { var imgs = document.querySelectorAll(".obj"); for (var i = 0; i < imgs.length; i++) { new FileUpload(imgs[i], imgs[i].file); } }
2行目で、imgs
という変数に、obj
という class
がつけられた要素のリストを格納しています。この場合、要素はすべて画像サムネイルになります。要素のリストを得られたら後は簡単です。リストを見ていき、ひとつのアイテムに対し FileUpload
インスタンスを生成すれば良いのです。それぞれのハンドラが該当するファイルをアップロードします。
ファイルのアップロードプロセス処理
FileUpload
関数は2つの引数を取ります。1番目は img
要素で、2番目が画像データを読むファイルになります。
function FileUpload(img, file) { var reader = new FileReader(); this.ctrl = createThrobber(img); var xhr = new XMLHttpRequest(); this.xhr = xhr; var self = this; this.xhr.upload.addEventListener("progress", function(e) { if (e.lengthComputable) { var percentage = Math.round((e.loaded * 100) / e.total); self.ctrl.update(percentage); } }, false); xhr.upload.addEventListener("load", function(e){ self.ctrl.update(100); var canvas = self.ctrl.ctx.canvas; canvas.parentNode.removeChild(canvas); }, false); xhr.open("POST", "https://demos.hacks.mozilla.org/paul/demos/resources/webservices/devnull.php"); xhr.overrideMimeType('text/plain; charset=x-user-defined-binary'); reader.onload = function(evt) { xhr.send(evt.target.result); }; reader.readAsBinaryString(file); }
FileUpload()
関数は throbber という、進行状況を表示するものを作ります。その後、 XMLHttpRequest
でデータをアップロードします。
データを実際に転送する前には、次のようなステップが実行されています。
XMLHttpRequest の
アップロードprogress
リスナが登録され、throbber に新しいパーセント値を設定します。こうすることでアップロードが進行しても、throbber が最新の状態を反映しますXMLHttpRequest
のアップロードload
イベントハンドラが登録され、throbber を100%に更新します (これは進行状況の表示がちゃんと100%になるように見せるためです)。そして、もう必要がなくなった throbber を削除します。つまりアップロードが終わると、throbber が消えるということです- 画像ファイルのアップロードリクエストは
XMLHttpRequest
のopen()
メソッドを呼び出し、POST リクエストを開始させます - アップロードの MIME タイプは
XMLHttpRequest
のoverrideMimeType()
メソッドで設定します。この場合は一般的な MIME タイプを設定しています。何をするかによりますが、MIME タイプを指定しなくてもいい場合もあります - ファイルをバイナリ文字列に変換するため、
FileReader
オブジェクトを使用します - 最後に、内容が読み込まれたら
XMLHttpRequest
のsendAsBinary()
メソッドが呼び出され、ファイルをアップロードします
Handling the upload process for a file, asynchronously (deprecated getAsBinary)
function fileUpload(file) { // Please report improvements to: marco.buratto at tiscali.it var fileName = file.name, fileSize = file.size, fileData = file.getAsBinary(), // works on TEXT data ONLY. use new FileReader boundary = "xxxxxxxxx", uri = "serverLogic.php", xhr = new XMLHttpRequest(); xhr.open("POST", uri, true); xhr.setRequestHeader("Content-Type", "multipart/form-data, boundary="+boundary); // simulate a file MIME POST request. xhr.setRequestHeader("Content-Length", fileSize); xhr.onreadystatechange = function() { if (xhr.readyState == 4) { if ((xhr.status >= 200 && xhr.status <= 200) || xhr.status == 304) { if (xhr.responseText != "") { alert(xhr.responseText); // display response. } } } } var body = "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name='fileId'; filename='" + fileName + "'\r\n"; body += "Content-Type: application/octet-stream\r\n\r\n"; body += fileData + "\r\n"; body += "--" + boundary + "--"; xhr.send(body); return true; }
This needs to be modified for working with binary data, too.