jQuery – 画像アップロード時のプレビュー表示(HTML5 File API)

前回の投稿(画像ファイルのアップロードの備忘ソース)でhtmlフォームから複数枚の画像ファイルを選択して、サーバにアップロードするソースを紹介しました。

が、その後自分で色々試験していくうちに、やはり2018/4/30現在のAndroidだと複数枚画像選択がイケてない状態であることがわかりました。

というのも、おそらくスマホ内の画像ディレクトリが写真を撮影したデータ、WebやLINEなどから保存したデータなどで散在しており、全ての画像があるディレクトリ(フォト?らいぶらり?)を選択すると結局1枚しか選択できない事象が発生しました。うーん、androidめっ・・・

苦肉の策ですが、input type=fileを増やしていく作戦を取ることにしたのですが、入力フォームが複数あるとどこでどれを選んだか分かりにくいなぁ~と思って、プレビューしようと思ったり、じゃあ選択したやつやっぱ取りやめ~的なこともできないとダメじゃん~っとなったりで、もろもろ修正が入りました。

前回のに追加してもよかったのですが、長くなったので別記事にします。

今回のやりたいことは、

(1)画像選択は複数できる(前回のmultiple属性と同じ)
(2)が、1枚しか選択できないAndroidのためにフォームを追加できるようにする
(3)ただし、↑はUSER_AGENTを見てAndroidのみ
(4)POSTする前にどれを選択しているかプレビュー表示する
(5)取り消し的な操作をすると選択がクリアされる

です、正直かなり苦肉の策感がハンパないので、相当ダサいソースだと思います・・・(File APIのところは調べて出てきた記事を引用したのですが、jQueryのタグ操作、classやidの使い方などがセンスないな~と思っています。。)

 

サンプルソース

まずはhtmlのソース

<form action="./image_upload_received" name="form1" method="POST" enctype="multipart/form-data">
    
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000">
    
    <div class="upload_image_container">
        <label class="image_select_icon" id="image0">
            <input type="file" id="select_file0" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
            <img src="./image/image_select.png"><p>画像を選択</p>
            <a href="javascript:void(0)" onclick="clearValue('select_file0')" class="invisible_container" id="delete_button0">取り消し</a>
        </label>
        <div class="upload_preview_container">
            <div class="preview0"></div>
        </div>
    </div>
    <div class="upload_image_container invisible_container">
        <label class="image_select_icon" id="image1">
            <input type="file" id="select_file1" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
            <img src="./image/image_select.png"><p>画像を選択</p>
            <a href="javascript:void(0)" onclick="clearValue('select_file1')" class="invisible_container" id="delete_button1">取り消し</a>
        </label>
        <div class="upload_preview_container">
            <div class="preview1"></div>
        </div>
    </div>
    <div class="upload_image_container invisible_container">
        <label class="image_select_icon" id="image2">
            <input type="file" id="select_file2" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
            <img src="./image/image_select.png"><p>画像を選択</p>
            <a href="javascript:void(0)" onclick="clearValue('select_file2')" class="invisible_container" id="delete_button2">取り消し</a>
        </label>
        <div class="upload_preview_container">
            <div class="preview2"></div>
        </div>
    </div>
    <div class="upload_image_container invisible_container">
        <label class="image_select_icon" id="image3">
            <input type="file" id="select_file3" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
            <img src="./image/image_select.png"><p>画像を選択</p>
            <a href="javascript:void(0)" onclick="clearValue('select_file3')" class="invisible_container" id="delete_button3">取り消し</a>
        </label>
        <div class="upload_preview_container">
            <div class="preview3"></div>
        </div>
    </div>
    
    <?php
    $ua = $_SERVER["HTTP_USER_AGENT"];
    if( preg_match( "/Android/", $ua) && preg_match( "/Mobile/", $ua) ){
        print "
            <label class=\"image_select_icon\">
                <a href=\"javascript:void(0)\" onclick=\"addImage()\"><img src=\"./image/plus_icon.png\">追加する</a>
            </label>
        ";
    }
    ?>
    
    <a href="javascript:void(0)" onclick="input_check()">アップロードする</a><br>
</form>

 
続いてCSS、CSS側で最初は見えないようにしています

.invisible_container {
    display: none;
}
label.image_select_icon {
    display: block;
    margin: 5px 0 5px 0;
    line-height: 30px;
}
label.image_select_icon img {
    display: inline-block;
    vertical-align: top;
    width: 30px;
}
label.image_select_icon p {
    display: inline-block;
    vertical-align: top;
    margin: 0.53px 0 0 5px;
}
label.image_select_icon a {
    color: #000000;
    text-decoration: none;
}
div.upload_preview_container {
    width: 200px;
}

 
最後にjQueryです

//プレビュー
$(function(){
    $('form').on('change', 'input[type="file"]', function(evt) {
        var files = evt.target.files;
        
        //何個目のinput type fileか?
        var select_id = $(this).attr('id');
        var id = select_id.slice(-1);
        
        //対応するプレビュー領域
        var prev_name = '.preview' + id;
        var $preview = $(prev_name);
        
        //既存のプレビューを削除
        $preview.empty();
        
        for (var i = 0, f; f = files[i]; i++) {
            // Only process image files.
            if (!f.type.match('image.*')) {
                continue;
            }
            
            var reader = new FileReader();
            
            // Closure to capture the file information.
            reader.onload = (function(theFile) {
                return function(e) {
                    //.prevewの領域の中にロードした画像を表示するimageタグを追加
                    $preview.append($('<img>').attr({
                        src: e.target.result,
                        width: "100px",
                        class: "preview",
                        title: theFile.name
                    }));
                };
            })(f);
            
            // Read in the image file as a data URL.
            reader.readAsDataURL(f);
        }
        
        //1枚以上選択されていたら取り消しボタンを出す、0枚だったら出さない
        if(files.length > 0){
            $('#delete_button'+id).removeClass('invisible_container');
        }
    });
});
function clearValue(id){
    var id_num = id.slice(-1);
    $('.preview'+id_num).empty();
    $("#"+id).replaceWith('<input type="file" id="' + id + '" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>');
    //$("#"+id).replaceWith($("#"+id).clone(true));
    //console.log( $("#"+id).attr('accept') );
    $('#delete_button'+id_num).addClass('invisible_container');
}
function addImage(){
    var remove_flg = 0;
    $('.upload_image_container').each(function(){
        var regex = /invisible_container/;
        if( regex.test( $(this).attr('class') ) && remove_flg == 0 ){
            $(this).removeClass('invisible_container');
            remove_flg = 1;
        }
    });
}

 

解説

それでは1コずつ解説していきます

 

(1)画像選択は複数できる

前回の記事で書いてますので、詳しくはそちらを。今回は前回の記事でもAndroidだと実現できない部分のフォローになります。

 

(2)が、1枚しか選択できないAndroidのためにフォームを追加できるようにする

原始的です・・・( ´・ω・`)
以下抜粋しますが、upload_image_containerというclass名のdivが1つのフォームのかたまりで、初期状態では2つ目以降にinvisible_containerというclassもつけています。cssでinvisible_containerはdisplay:noneに設定していますので、見えないだけです。

<div class="upload_image_container">
    <label class="image_select_icon" id="image0">
        <input type="file" id="select_file0" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
        <img src="./image/image_select.png"><p>画像を選択</p>
        <a href="javascript:void(0)" onclick="clearValue('select_file0')" class="invisible_container" id="delete_button0">取り消し</a>
    </label>
    <div class="upload_preview_container">
        <div class="preview0"></div>
    </div>
</div>
<div class="upload_image_container invisible_container">
    <label class="image_select_icon" id="image1">
        <input type="file" id="select_file1" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>
        <img src="./image/image_select.png"><p>画像を選択</p>
        <a href="javascript:void(0)" onclick="clearValue('select_file1')" class="invisible_container" id="delete_button1">取り消し</a>
    </label>
    <div class="upload_preview_container">
        <div class="preview1"></div>
    </div>
</div>

 

っで、以下phpで書いていますが、androidで画面表示すると追加するボタンを表示するようにしています。

$ua = $_SERVER["HTTP_USER_AGENT"];
if( preg_match( "/Android/", $ua) && preg_match( "/Mobile/", $ua) ){
    print "
        <label class=\"image_select_icon\">
            <a href=\"javascript:void(0)\" onclick=\"addImage()\"><img src=\"./image/plus_icon.png\">追加する</a>
        </label>
    ";
}

 

コイツが押されると以下のfunctionが呼び出されて、invisible_containerが削除されて見えるようになる、原始的ですね( ´・ω・`)
ちなみに最初、以下のaddImage()は当初eachの中で、最初にinvisible_containerが見つかったらremoveClassしてexitしようと思ったのですが、jsにはexitってなかったんですね・・・try – catchでexit相当を実装するとか調べたら出てきたのですが、面倒だったのでフラグにしてしまいました。長いループだと遅くなりそうです。

function addImage(){
    var remove_flg = 0;
    $('.upload_image_container').each(function(){
        var regex = /invisible_container/;
        if( regex.test( $(this).attr('class') ) && remove_flg == 0 ){
            $(this).removeClass('invisible_container');
            remove_flg = 1;
        }
    });
}

 

ただ、このやり方だと最初からhtmlにinvisible_container部分も記述しておかないといけないので、相当な数のフォームを用意したい場合だと何かしらの方法で動的に生成できる方がよさそうですね、わたしは上限はまあ多くて4枚くらいの写真かなーと思っていたので、この方法にしました。

 

(3)ただし、↑はUSER_AGENTを見てAndroidのみ

↑で既に説明済みですが、$_SERVER[“HTTP_USER_AGENT”]を見てAndroidか判定します。中身には以下のようなモノが入っています。

Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; Nexus One Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1

わたしはAndroidスマホだけ対象にしたかったので、preg_matchの反映にMobileを入れましたが、タブレットも対象にしたい場合はAndroidだけで良いようです。

 

(4)POSTする前にどれを選択しているかプレビュー表示する

今回の肝の部分です、HTML5のFile APIというのを使います。
ここは細かく解説します。

$(function(){
    $('form').on('change', 'input[type="file"]', function(evt) {
        var files = evt.target.files;

input type=”file”のonchangeイベントを検知して、jQueryイベントをfunctionに渡します。そしてそのイベントのファイルオブジェクトをぶちこみます。

 

//何個目のinput type fileか?
var select_id = $(this).attr('id');
var id = select_id.slice(-1);

//対応するプレビュー領域
var prev_name = '.preview' + id;
var $preview = $(prev_name);

//既存のプレビューを削除
$preview.empty();

ここもダサい感じで恥ずかしいのですが、input typeのタグにid属性で連番を付与して、何個目のフォームなのかわかるようにしました。既に画像が選択済みでプレビュー表示されていた場合、もっかい選択され直したら、最初に前回のプレビュー表示をここでクリアしておきます。

 

for (var i = 0, f; f = files[i]; i++) {
    // Only process image files.
    if (!f.type.match('image.*')) {
        continue;
    }
    
    var reader = new FileReader();
    
    // Closure to capture the file information.
    reader.onload = (function(theFile) {
        return function(e) {
            //.prevewの領域の中にロードした画像を表示するimageタグを追加
            $preview.append($('<img>').attr({
                src: e.target.result,
                width: "100px",
                class: "preview",
                title: theFile.name
            }));
        };
    })(f);
    
    // Read in the image file as a data URL.
    reader.readAsDataURL(f);
}

複数枚画像が選択された場合を考慮してforでまわします。
FileReader()というやつがFile APIですね、コイツを生成して、画像の読み込みが完了したら発火するonloadイベント使います。onloadイベントではpreview領域の中に、imgタグを作ってe.target.resultをsrcとします。ここに読み込んだが画像が入っています。

 

//1枚以上選択されていたら取り消しボタンを出す、0枚だったら出さない
if(files.length > 0){
    $('#delete_button'+id).removeClass('invisible_container');
}

次の(5)で説明しますが、画像を選択したら、ここで取り消しボタンを表示させます。

 

(5)取り消し的な操作をすると選択がクリアされる

最後にコチラ。
twitterとかで画像の右上に×印があり、選択した画像を取りやめることが出来ることにあとで気づきました。input type=”file”が1つだけで、multipleがうまく機能していれば、選択し直してもらうだけで良かったんですが、2つ以上フォームがある場合は削除できた方がいいなーと思って用意しました。

function clearValue(id){
    var id_num = id.slice(-1);
    $('.preview'+id_num).empty();
    $("#"+id).replaceWith('<input type="file" id="' + id + '" accept="image/jpeg, image/gif, image/png" name="upfile[]" multiple="multiple" hidden>');
    //$("#"+id).replaceWith($("#"+id).clone(true));
    //console.log( $("#"+id).attr('accept') );
    $('#delete_button'+id_num).addClass('invisible_container');
}

先ほど表示させた取り消しボタンを押すとこちらが呼ばれます。
最初調べた時、replaceWithにcloneを突っ込むだけで消える~っていう感じの記事が多かったのですが、わたしはうまくいかなかったので、input type=”file”のタグを全部作り直すようにしました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です