13
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-04-01

第三回に引き続き、掲示板の作成を行っていきます。

今回は

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

#目次
~第一回 CRUDの実装~
~第二回 paginate, ナビゲーションバーの実装~
~第三回 画像投稿機能の実装~
~第四回 ドラッグアンドドロップ(DnD)での画像添付 ~

#やりたいこと

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

以下の様なドロップエリアが完成します。
スクリーンショット 2016-04-02 0.19.39.png

ファイルをドラッグしたままドロップエリアに侵入すると、エリアの枠が変化します
名称未設定.png

ファイルをドロップすると選択状態になります。
スクリーンショット 2016-04-02 0.19.55.png

ドロップエリアをクリックすると、↑ボタンと同じ挙動を行います
スクリーンショット 2016-04-02 0.20.29.png

#実装の方針

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

#HTML

  • 要素(ドロップエリア・inputボタン)を重ねるためにwrapperclassを持たせた<div>タグで囲みます
    • preview_fieldid : 選択中のファイルの縮小版表示用タグ
    • 'text_drop'id : エリア内のテキストを表示用のタグ
    • drop_areaid : ドロップエリアを生成用のタグ
    • input_fileid : ファイル参照ボタン生成用のタグ
      • 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%に設定しているのは、4隅をダブルクリックする余地を残すためです
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
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には、アップロードできる画像の拡張子を格納しています
    //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ボタンの表示の制御を行っています
    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ボタンを有効化しています
    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ボタンがクリックされ、ファイル選択が行われたとき、もしくは選択中のファイルが書き換わったときこのイベントが発火します
    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が1のとき、つまりエラーに引っかからなかったとき
    • エリア内のテキスト書き換え
    • サムネイルの表示関数の呼び出し
  • flagが1ではないとき
    • エラーの種類によって表示するエラーメッセージの内容を設定します
    • inputタグの値を空にします
    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形式にして設定しています
    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

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?