JavaScriptで特定の要素に画像ファイルをドラッグ＆ドロップできる処理を書く

#はじめに

HTML5になって画像ファイルを特定の要素にドラッグ＆ドロップできるような処理ができるようになりました。
どんな感じなのか、複数の画像をページ上に表示させるだけのお試しコードを書いてみました。
ついでに、`type="file"` なinput要素でのファイル参照にも対応させてみます。

#書いてみる

複数の画像ファイルをまとめてドラッグ＆ドロップするとそれらの画像が全部表示されるだけの処理です。

```html:HTML
<div id="dropArea">Drop or Click here!</div>
<input id="fileInput" type="file" accept="image/*" multiple>
<div id="output"></div>
```

```css:CSS
#dropArea {
  overflow: hidden;
  padding: 40px 10px;
  background: #ddd;
  border: 3px #666 dashed;
  color: #999;
  font-size: 30px;
  font-weight: bold;
  text-align: center;
}

#dropArea.dragover,
#dropArea:active {
  background: #cfc;
  border-color: #9c9;
  color: #9c9;
}

#fileInput {
  display: none;
}

#output img {
  display: block;
  margin: 15px auto;
}
```

```js:JavaScript
document.addEventListener('DOMContentLoaded', function () {
  var
    dropArea = document.getElementById('dropArea'),
    output = document.getElementById('output'),

    // 画像の最大ファイルサイズ（20MB）
    maxSize = 20 * 1024 * 1024;

  // ドロップされたファイルの整理
  function organizeFiles(files) {
    var
      length = files.length,
      i = 0, file;

    for (; i < length; i++) {
      // file には Fileオブジェクト というローカルのファイル情報を含むオブジェクトが入る
      file = files[i];

      // 画像以外は無視
      if (!file || file.type.indexOf('image/') < 0) {
        continue;
      }

      // 指定したサイズを超える画像は無視
      if (file.size > maxSize) {
        continue;
      }

      // 画像出力処理へ進む
      outputImage(file);
    }
  }

  // 画像の出力
  function outputImage(blob) {
    var
      // 画像要素の生成
      image = new Image(),

      // File/BlobオブジェクトにアクセスできるURLを生成
      blobURL = URL.createObjectURL(blob);

    // src にURLを入れる
    image.src = blobURL;

    // 画像読み込み完了後
    image.addEventListener('load', function () {
      // File/BlobオブジェクトにアクセスできるURLを開放
      URL.revokeObjectURL(blobURL);

      // #output へ出力
      output.appendChild(image);
    });
  }

  // ドラッグ中の要素がドロップ要素に重なった時
  dropArea.addEventListener('dragover', function (ev) {
    ev.preventDefault();

    // ファイルのコピーを渡すようにする
    ev.dataTransfer.dropEffect = 'copy';

    dropArea.classList.add('dragover');
  });

  // ドラッグ中の要素がドロップ要素から外れた時
  dropArea.addEventListener('dragleave', function () {
    dropArea.classList.remove('dragover');
  });

  // ドロップ要素にドロップされた時
  dropArea.addEventListener('drop', function (ev) {
    ev.preventDefault();

    dropArea.classList.remove('dragover');
    output.textContent = '';

    // ev.dataTransfer.files に複数のファイルのリストが入っている
    organizeFiles(ev.dataTransfer.files);
  });

  // #dropArea がクリックされた時
  dropArea.addEventListener('click', function () {
    fileInput.click();
  });

  // ファイル参照で画像を追加した場合
  fileInput.addEventListener('change', function (ev) {
    output.textContent = '';

    // ev.target.files に複数のファイルのリストが入っている
    organizeFiles(ev.target.files);

    // 値のリセット
    fileInput.value = '';
  });
});
```

サンプルも置いておきます。
[画像ファイルを複数ドロップしたらそれらが表示されるだけのサンプル](https://sdn-project.net/sample/dragdrop.html)

一気にコードを書いてしまいましたが、ひとつひとつ紐解いていきます。

##dragoverイベント

指定した要素にドラッグ中の要素が重なる呼び出されるイベントです。

ここで気になるのが `Event.dataTransfer.dropEffect` ですが、これは指定した要素にドラッグした時にどういった目的のものなのかを示すものです。
複数種類がありますが、今回はローカルからのコピーという扱いなので `Event.dataTransfer.dropEffect` の値は `'copy'` にしています。

##dragleaveイベント

指定した要素からドラッグ中の要素が出ると呼び出されるイベントです。

##dropイベント

指定した要素にドロップされた時に呼び出されるイベントです。

要素にドロップされたファイルのリストを取得するには `Event.dataTransfer.files` を使用します。
このファイルのリストは配列に似た FileListオブジェクト というものになっていて、そのリストの中身はそれぞれのファイルのBlobオブジェクトが入っています。

##Fileオブジェクト, Blobオブジェクト

FileオブジェクトとBlobオブジェクトはファイルの形式やファイル名、ファイルサイズなどのファイル情報が含まれたバイナリデータからなるオブジェクトです。
二つの違いは、Fileオブジェクトがローカルのファイルを取り扱うのに対して、Blobオブジェクトはローカルのファイルを取り扱えません。それ以外はほぼ同じです。

##`URL.createObjectURL()`

ブラウザ内で持っているFile/BlobオブジェクトへアクセスできるURLを生成するメソッドです。
そのページ上でのみ利用可能な特殊なURLです。

##`URL.revokeObjectURL()`

File/BlobオブジェクトへアクセスできるURLを開放するメソッドです。
引数にBlobのURLを指定するとそのURLは無効になり、そのURLにアクセスできなくなります。

#おわりに

ドラッグ＆ドロップの処理さえ覚えたらファイルのアップロード等の処理も書けるようになっていろいろ楽しいことができそうです。