CakePHP3で画像投稿機能付き掲示板作成 ~第四回 ドラッグアンドドロップ(DnD)での画像添付 ~

[第三回](http://qiita.com/banaba0207/items/6cee4cd74b9231ca52c4)に引き続き、掲示板の作成を行っていきます。

今回は
- ドラッグアンドドロップによる画像ファイルの添付
を実装していきます。

#目次
[~第一回 CRUDの実装~](http://qiita.com/banaba0207/items/3610f3f24f4f03e3b93f)
[~第二回 paginate, ナビゲーションバーの実装~](http://qiita.com/banaba0207/items/b05f0942d39792c412c5)
[~第三回 画像投稿機能の実装~](http://qiita.com/banaba0207/items/6cee4cd74b9231ca52c4)
[~第四回 ドラッグアンドドロップ(DnD)での画像添付 ~](http://qiita.com/banaba0207/items/ec5dd68413534eef938a)

#やりたいこと
- ファイルをドロップエリアにDnDすると、ファイル選択状態に移行する
    - その際、サムネイルを表示する
- ドロップエリアをクリックすることでも、ファイル選択を行うことができる
- 選択できるファイルは1つまで
    - jpg, png, gif ファイルがアップロード可能
    - 10M以下のファイルをアップロード可能
- 選択されたファイルを、他のフォーム（タイトル、名前、本文）と一緒にPOSTで送信する

以下の様なドロップエリアが完成します。
![スクリーンショット 2016-04-02 0.19.39.png](https://qiita-image-store.s3.amazonaws.com/0/111756/daea7019-8589-2071-0efb-6bffa481f193.png)

ファイルをドラッグしたままドロップエリアに侵入すると、エリアの枠が変化します
![名称未設定.png](https://qiita-image-store.s3.amazonaws.com/0/111756/78f5ee7a-ea72-fa68-ba98-d9eae91eb1bd.png)

ファイルをドロップすると選択状態になります。
![スクリーンショット 2016-04-02 0.19.55.png](https://qiita-image-store.s3.amazonaws.com/0/111756/f2f67f43-ac9f-e27f-6c80-2e038c2f2f00.png)

ドロップエリアをクリックすると、<input type='file'>↑ボタンと同じ挙動を行います
![スクリーンショット 2016-04-02 0.20.29.png](https://qiita-image-store.s3.amazonaws.com/0/111756/2e918e1a-5c5d-1897-68fb-faf1d900e415.png)

#実装の方針
- HTML5のFileAPIとDrag & Drop API, jQueryを組み合わせてドラッグアンドドロップ（DnD)を実装する
    - ドロップエリアと`<input type='file'>`で生成したボタンを同じ大きさにする
    - ボタンを透明にして、ドロップエリアの上に重ねる
- DnD操作
    - ドラッグ操作を行っていないとき
        - inputボタンを表示（有効化）する
        - ドロップエリアの枠を点線に変化させる
    - ドロップエリアの`drag over`イベントを察知したとき
        - inputボタンを非表示（無効化）にする
        - ドロップエリアの枠を太線に変化させる
    - ドロップエリアの`drag leave`イベントを察知したとき
        - inputボタンを表示（有効化）する
        - ドロップエリアの枠を点線に変化させる
    - ドロップエリアの`drop`イベントを察知したとき
        - `<input>`タグの中身をドロップされたファイルに差し替える
        - ドロップされたファイルの縮小版を表示する
        - エリア内のテキストを変化させる
    - ドロップエリアの４隅をダブルクリックしたとき
        - `<input>`タグの中身を空にする
        - ファイルの表示を消す
        - テキストをデフォルトの状態に戻す

#HTML
- 要素（ドロップエリア・inputボタン）を重ねるために`wrapper`classを持たせた`<div>`タグで囲みます
    - `preview_field`id : 選択中のファイルの縮小版表示用タグ
    - 'text_drop'id : エリア内のテキストを表示用のタグ
    - `drop_area`id : ドロップエリアを生成用のタグ
    - `input_file`id : ファイル参照ボタン生成用のタグ
        - `accept`オプションで”クリックでのファイル選択時のみ”画像ファイルしか選択できない状態にします
- jQueryによって各タグにイベントや処理をひも付けていきます。

ドロップエリア部分のソースは以下になります。

```php.src/Template/Element/form.ctp
<div class="wrapper">
    <div id="preview_field"></div>
    <div id='text_drop'></div>
    <div id="drop_area"></div>
    <input type="file" id="input_file" name="img" accept='image/*'>
</div>
~~
```

#CSS
- wrapperでエリアの大きさを設定しています
    - input_file の設定でクリックできる範囲を 70%に設定しているのは、４隅をダブルクリックする余地を残すためです

```css.webroot/css/my_drop.css
.wrapper{
    position: relative;
    width: 600px;
    height: 250px;
    text-align : center;
}

#drop_area {
    position: absolute;
    width: 100%;
    height: 100%;
    border: 3px blue dashed;  /* 枠線を引く */
    border-radius: 10px;      /* 角丸の指定 */
}

#input_file {
    position: absolute;
    width: 70%; height: 70%; /* クリックできる範囲 */
    opacity: 0; /* inputの領域を透明に */
    margin-left: 15%;
    margin-top: 5%;
}

#text_drop {
    position: absolute;
    top: 5%; left: 100px; /* テキストの位置設定 */
    font-size: 150%;
    color: #AAAAAA; /* エリア内のテキストの色 */
}

#preview_field {
    position: absolute;
    opacity: 0.5;　/* 表示する画像を半透明にする */
    top: 37%; left: 40%; /* プレビュー画像の位置設定 */
}

```

※input_file id の opacityを0.5にした場合、以下の様な表示になります。
![スクリーンショット 2016-04-02 1.20.22.png](https://qiita-image-store.s3.amazonaws.com/0/111756/00ee9da3-2e5e-d3ac-3da0-ef7f446f7a57.png)
opacityを0に設定することにより、見かけ上ファイル選択ボタンを見えなくしています。

#jQuery
まず全てのソースを貼り付けておきます。
例によってブサイクなコードですが、ご容赦下さい。
細かい解説は後のほうで。

```js.webroot/js/my_DnD.js
(function(){
    // サポートチェック
    if(!(window.addEventListener)) return;
    if(!(window.File)) return;
    if(!(window.FormData)) return;

    //Message----------------
    var pre_drop_area = '画像ファイルをドロップするか <br>中心部分をクリックしてください.' ;
    var upl_drop_area = '四隅をダブルクリックすると<br>添付画像をキャンセルできます.';
    //----------------------
    // 各要素を取得する
    var drop_area= $("#drop_area");
    var text_drop = $("#text_drop");
    text_drop.html(pre_drop_area);
    var input_file = $('#input_file');

    //validation用の配列-------------------
    var acceptExt = ['image/jpeg', 'image/png', 'image/gif'];

    drop_area.on('dragleave', function (e) {
        e.stopPropagation();//後続へのイベント伝播を止める
        e.preventDefault();//イベントのデフォルト処理をキャンセルする
        $(this).css('border', '3px dashed blue');
        input_file.show(); //inputフィールドを表示
    });

    drop_area.on('dragenter dragover', function (e) {
        e.stopPropagation();
        e.preventDefault();
        $(this).css('border', '7px solid blue');
        input_file.hide(); //inputフィールドを隠す
    });

    drop_area.on('drop', function (e) {
        e.preventDefault();
        //$(#input_field) の changeイベントを発火させる
        $('#input_file')[0].files = e.originalEvent.dataTransfer.files;
        input_file.show(); //inputフィールドを表示
    });

    drop_area.on('dblclick', function (e) {
        e.stopPropagation();
        e.preventDefault();
        $('input[type=file]').val('');
        text_drop.html(pre_drop_area); //エリア内のテキスト変更
        drop_area.css('border', '3px dashed blue');

        $('#preview_field').empty(); //サムネイルの削除
    });

    input_file.on("change", function(e) {
        var file_list = $(this)[0].files; // var file_list = this.files; でも代替可能
        setFile(file_list);
    });

    function setFile(files) {
        $('#preview_field').empty(); //サムネイルの削除
        drop_area.css('border', '3px dashed blue');
        var flag = 1;
        if(files.length != 1) //アップロードされた画像が1つかどうか
            flag = -1;
        if($.inArray(files[0].type, acceptExt) == -1) //使用できる拡張子か
            flag = -2;
        if(files[0].size >= 10485760) //10Mより小さいか
            flag = -3;
        if(flag == 1) {
            drop_area.css('text-align', 'center');
            text_drop.html(upl_drop_area); //エリア内のテキスト変更
            setPreview(files[0]);
        } else {
            var err_text = '';
            if(flag == -1)
                err_text = '一度にアップロードできる<br>画像ファイルは1つのみです.';
            else if(flag == -2)
                err_text = 'jpg, png, gif形式の画像ファイルのみ<br>アップロードできます.';
            else if(flag == -3)
                err_text = '10M以下の画像ファイルのみ<br>アップロードできます.';
            err_text += '<br>' + pre_drop_area;
            text_drop.html(err_text); //エリア内のテキスト変更
            $('input[type=file]').val('');
        }
    }

    function setPreview(file) {
        var img = document.createElement('img');
        img.height = 150;
        var fr = new FileReader();
        fr.onload = function() {
            img.src = fr.result;  // 読み込んだ画像データをsrcにセット
            $('#preview_field').append(img);
        }
        fr.readAsDataURL(file);  // 画像読み込み
    }

    //Drop area以外でファイルがドロップされた場合、ファイルが開いてしまうのを防ぐ
    $(document).on('dragenter', function (e) {
        e.stopPropagation();
        e.preventDefault();
    });
    $(document).on('dragover', function (e) {
        e.stopPropagation();
        e.preventDefault();
    });
    $(document).on('drop', function (e) {
        e.stopPropagation();
        e.preventDefault();
    });
})();
```

##サポートチェック・エリア内テキストの設定・各要素の取得部分
- `$('#text_drop').html(~~)`では idがtext_dropであるタグ内のHTMLを与えられた引数で書き換えています
- `acceptExt`には、アップロードできる画像の拡張子を格納しています

```js
    //Message----------------
    var pre_drop_area = '画像ファイルをドロップするか <br>中心部分をクリックしてください.' ;
    var upl_drop_area = '四隅をダブルクリックすると<br>添付画像をキャンセルできます.';
    //----------------------
    // 各要素を取得する
    var drop_area= $("#drop_area");
    var text_drop = $("#text_drop");
    text_drop.html(pre_drop_area); //デフォルトテキストの設定
    var input_file = $('#input_file');

    //validation用の配列-------------------
    var acceptExt = ['image/jpeg', 'image/png', 'image/gif'];
```

##ドラッグ中の挙動制御部分
- 主にCSSとinputボタンの表示の制御を行っています


```js
    drop_area.on('dragleave', function (e) {
        e.stopPropagation();//後続へのイベント伝播を止める
        e.preventDefault();//イベントのデフォルト処理をキャンセルする
        $(this).css('border', '3px dashed blue');
        input_file.show(); //inputフィールドを表示
    });

    drop_area.on('dragenter dragover', function (e) {
        e.stopPropagation();
        e.preventDefault();
        $(this).css('border', '7px solid blue');
        input_file.hide(); //inputフィールドを隠す
    });
```

##ファイルがエリア内にdropされた際の挙動制御部分
- ドロップされたファイルを`e.originalEvent.dataTransfer.files`で取得し、inputタグの値に代入しています
    - この操作により、inputタグのchangeイベントを発火させています
- ファイルドロップ後、無効化されていたinputボタンを有効化しています
    
```js
    drop_area.on('drop', function (e) {
        e.preventDefault();
        //$(#input_field) の changeイベントを発火させる
        $('#input_file')[0].files = e.originalEvent.dataTransfer.files;
        input_file.show(); //inputフィールドを表示
    });
```

##inputタグの値が変わった時の挙動
- inputボタンがクリックされ、ファイル選択が行われたとき、もしくは選択中のファイルが書き換わったときこのイベントが発火します

```js
    input_file.on("change", function(e) {
        var file_list = $(this)[0].files; // var file_list = this.files; でも代替可能
        setFile(file_list);
    });
```

## ファイルが選択されたときの挙動
- サムネイル表示部分（preview_field id）内のHTMLを空にします
- 選択されたファイルのvalidationを行います
    - エラーの種類によってflagの値を書き換えます
- flagが１のとき、つまりエラーに引っかからなかったとき
    - エリア内のテキスト書き換え
    - サムネイルの表示関数の呼び出し
- flagが1ではないとき
    - エラーの種類によって表示するエラーメッセージの内容を設定します
    - inputタグの値を空にします

```js
    function setFile(files) {
        $('#preview_field').empty(); //サムネイルの削除
        drop_area.css('border', '3px dashed blue');
        var flag = 1;
        if(files.length != 1) //アップロードされた画像が1つかどうか
            flag = -1;
        if($.inArray(files[0].type, acceptExt) == -1) //使用できる拡張子か
            flag = -2;
        if(files[0].size >= 10485760) //10Mより小さいか
            flag = -3;
        if(flag == 1) {
            drop_area.css('text-align', 'center');
            text_drop.html(upl_drop_area); //エリア内のテキスト変更
            setPreview(files[0]);
        } else {
            var err_text = '';
            if(flag == -1)
                err_text = '一度にアップロードできる<br>画像ファイルは1つのみです.';
            else if(flag == -2)
                err_text = 'jpg, png, gif形式の画像ファイルのみ<br>アップロードできます.';
            else if(flag == -3)
                err_text = '10M以下の画像ファイルのみ<br>アップロードできます.';
            err_text += '<br>' + pre_drop_area;
            text_drop.html(err_text); //エリア内のテキスト変更
            $('input[type=file]').val('');
        }
    }
```

- `<img>`タグを生成し、`src`オプションに受け取ったファイルをbase64形式にして設定しています

```js
    function setPreview(file) {
        var img = document.createElement('img');
        img.height = 150;
        var fr = new FileReader();
        fr.onload = function() {
            img.src = fr.result;  // 読み込んだ画像データをsrcにセット
            $('#preview_field').append(img);
        }
        fr.readAsDataURL(file);  // 画像読み込み
    }
```

#まとめ
今回、ドラッグアンドドロップでファイルを選択できるように実装を行いました。
jQueryやJavascriptは難しそうなイメージがあり敬遠していましたが、これを機に、深く取り組んでみようと思います。
アドバイスやよりエレガントな方法をご存知の方はぜひコメント欄にてお願いいたしますm(__)m