DOMドラッグ&ドロップファイル操作同期・非同期

【JavaScript】 画像ファイルのドラッグ&ドロップとimgタグ/canvasへの描画

更新日:2020/07/27

今回はブラウザに画像ファイルをドラッグ&ドロップして、画像のリストを作成します。
その後、リストから選択した画像をキャンバスに描画します。

 

仕様/デモ

ブラウザのドロップエリアに画像ファイルがドロップされたとき、次の条件でファイルの内容を表示します。

(1) imgタグを作成して、画像のリストを表示する

(2) 画像リストをクリックで選択可能にして、選択した画像をキャンバスに描画する

(3) 複数ファイルの一括ドロップには対応しません。

■完成DEMO

点線の四角内にファイルをドロップしてください。

ここにドロップ

 

完成コード

今回作成したコードです。

完成コード:html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>画像ファイルのドラッグ&ドロップとimgタグ/canvasへの描画</title>

    <style>
        body{
            min-height: 100vh;
        }
        #ddarea{
            width:200px;
            height:150px;
            border:1px dotted #888;
            margin: 1em auto;
        }
        #ddarea.ddefect{
            border:1px solid #000;
        }
        #imagearea{
            width: 90%;
            height: 350px;
            overflow: auto;
            background: black;
            color:white;
            padding: 1em;
        }
        #imagearea > div{
            border: 1px solid white;
            padding: 5px;
            margin: 1px;
            display: flex;
        }
        #imagearea > div > div:first-of-type {
            margin: 0 10px 0 0;
        }
        #imagearea > div > div > img{
            width:150px;
        }
        #imagearea div > div > p{
            color: white;
        }
        #canvasarea{
            margin: 1em;
        }
        .itemselect{
            background: blue;
        }
    </style>
    <script>
    window.addEventListener( "DOMContentLoaded" , ()=> {

        const ddarea = document.getElementById("ddarea");
        const imagearea = document.getElementById("imagearea");
        const canvasContext = document.getElementById("canvasarea")
                                        .getContext("2d");

        let dropDisabled = false;

            // ドラッグされたデータが有効かどうかチェック
        const isValid = e => e.dataTransfer.types.indexOf("Files") >= 0
            && e.dataTransfer.items.length<=1
            && ( e.dataTransfer.items.length === 0 
                     || e.dataTransfer.items[0].type.indexOf("image/") >= 0 );

        const ddEvent = {
            "dragover" : e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める

                if( dropDisabled || !e.currentTarget.isEqualNode( ddarea ) ) {
                        // ドロップ機能停止中または ドロップエリア外ならドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }

                 if( !isValid(e) ){
                         // 無効なデータがドラッグされたらドロップを無効にする
                     e.dataTransfer.dropEffect = "none";return;
                 }

                        // ドロップのタイプを変更
                e.dataTransfer.dropEffect = "copy";
                ddarea.classList.add("ddefect");
            },
            "dragleave" : e=>{
                if( dropDisabled || !e.currentTarget.isEqualNode( ddarea ) ) {
                    return;
                }
                e.stopPropagation(); // イベント伝播を止める
                ddarea.classList.remove("ddefect");
            },
            "drop":e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める
                if(e.dataTransfer.files.length > 1) {
                    alert( "複数ファイルは対象外です");
                    return;
                }
                const file = e.dataTransfer.files[0];

                if(file.type.indexOf('image/') < 0) {
                    alert( file.name + "は画像ファイルではありません");
                    return;
                }

                dropDisabled = true;

                loadImageData( file ).then( e => {
                    ddarea.classList.remove("ddefect");
                    dropDisabled = false;
                });
            }
        };

        /**
         * 画像ファイル読み込み
         * @param targetFile
         * @returns {Promise<unknown>}
         */
        const loadImageData = targetFile => {

           return new Promise( (resolve, reject) => {

               const addDiv = document.createElement("div");
               addDiv.classList.add("itemarea");
               addDiv.innerHTML = `<div><img alt="${targetFile.name}"></div><div><p>${targetFile.name}(${targetFile.size}Bytes) [${targetFile.type}]</p></div>`;

               const imgTag = addDiv.getElementsByTagName("img")[0];
               const pTag = addDiv.getElementsByTagName("p")[0];

               imagearea.prepend( addDiv );
               const imageURL = URL.createObjectURL(targetFile);

               imgTag.onload = (e)=>{
                   URL.revokeObjectURL(imageURL);
                   pTag.innerHTML = `${pTag.textContent}<br>サイズ:幅 ${imgTag.naturalWidth} 高さ ${imgTag.naturalHeight}`;
                   resolve( true );
               };
               imgTag.onerror = e=>{
                   URL.revokeObjectURL(imageURL);
                   pTag.innerHTML = `${pTag.textContent}<br><span style="color:red">読み込めません</span>`;
                   resolve( false );
               };
               imgTag.src = imageURL;
           });
        };

        const itemAreaClick = e =>{
                
            Array.from(imagearea.getElementsByClassName("itemselect"))
                          .forEach( item => item.classList.remove("itemselect"));
            canvasContext.clearRect(0,0,400,400);

 
            let itemNode = e.target;
            while( !itemNode.classList.contains("itemarea")){
                if( itemNode.isEqualNode( e.currentTarget) ) return;
                itemNode = itemNode.parentNode;
            }

            itemNode.classList.add("itemselect");
            
            const imgTag = itemNode.getElementsByTagName("img")[0];
            canvasContext.drawImage(imgTag,0,0,400,400);
        };

        Object.keys( ddEvent ).forEach( e=>{
            ddarea.addEventListener(e,ddEvent[e]);
            document.body.addEventListener(e,ddEvent[e])
        });

        imagearea.addEventListener( "click" , itemAreaClick );

    });
    </script>
</head>
<body>

<div id="ddarea" ><p>ここにドロップ</p></div>

<div id="imagearea"></div>
<canvas id="canvasarea" width="400" height="400"></canvas>
</body>
</html>

ドラッグ部について

ブラウザにファイルをドラッグした際の処理は、次の記事で紹介しているものとほぼ同じです。
参考記事:【JavaScript】 ファイルをドラッグ&ドロップの基本形

ここでは解説していないので、参考記事を見てください。

なお今回はファイル読み込みに非同期処理をおこなうため、フラグ(dropDisabled)で読み込み中かどうか判断しています。
dropDisabledがtrueのとき、ドラッグを無効にしています。

ドラッグされたデータが有効かどうかチェック

次のコードで、ドラッグ中のデータが画像ファイルかどうかチェックしています。

データ有効チェック


        const isValid = e => e.dataTransfer.types.indexOf("Files") >= 0
            && e.dataTransfer.items.length<=1
            && ( e.dataTransfer.items.length === 0 
                                 || e.dataTransfer.items[0].type.indexOf("image/") >= 0 );

e.dataTransfer.types.indexOf("Files") >= 0

ファイルをドラッグしているとき、e.dataTransfer.typesに"Files"がセットされています。

e.dataTransfer.items.length<=1

ドラッグ中のデータファイル数を、e.dataTransfer.items.lengthで確認できます。
ただし一部のブラウザはドロップ後にデータがセットされるため、ドラッグ時は0になっています。
このとき判断基準がlength===1ななっているとファイルの受け入れができないので、判断基準を1以下としています。

( e.dataTransfer.items.length === 0 || e.dataTransfer.items[0].type.indexOf("image/") >= 0 )

e.dataTransfer.items.length<===1のとき、MIMEタイプに"image/"が含まれるかチェックしています。
MIMEタイプは拡張子で判断されます。
そのため、実際のデータが画像ではない可能性があります。

 

ドロップ時の処理

ブラウザにファイルがドロップされると、次のメソッドが呼び出されます。

ドロップイベントの処理


            "drop":e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める

                if(e.dataTransfer.files.length > 1) {
                    alert( "複数ファイルは対象外です");
                    return;
                }
                const file = e.dataTransfer.files[0];

                if(file.type.indexOf('image/') < 0) {
                    alert( file.name + "は画像ファイルではありません");
                    return;
                }

                dropDisabled = true;

                loadImageData( file ).then( e => {
                    ddarea.classList.remove("ddefect");
                    dropDisabled = false;
                });
            }

規定処理の中止とイベントの伝播をストップしたあと、dropDisabledにtrueをセットして、ファイル読み込み処理中にドロップイベントが発生しないようにします。

ドラッグ時にブラウザによってはチェック漏れがあるので、ドロップ時に再チェックをおこなっています。

続くloadData()は、ファイルを読み込みブラウザに表示する関数です。
この関数はPromiseオブジェクトを返します。

処理終了後resolve()を呼び出すので、then()でdropDisabledを無効にするなどの後処理をおこなっています。
参考記事:【JavaScript】 非同期はPromise?解説が難しいので自分で理解してみた

なおloadData()には、引数としてFileオブジェクトを渡しています。
e.dataTransfer.filesはドロップされたファイルの一覧で、個々のファイルは数値インデックスで参照します。

 

画像ファイルをimgタグにセット

次のコードは、画像ファイルをimgタグにセットしているloadImageData関数です。

画像ファイルをimgタグにセット

        /**
         * 画像ファイル読み込み
         * @param targetFile
         * @returns {Promise<unknown>}

         */
        const loadImageData = targetFile => {

           return new Promise( (resolve, reject) => {

               const addDiv = document.createElement("div");
               addDiv.classList.add("itemarea");
               addDiv.innerHTML = `<div><img alt="${targetFile.name}"></div><div><p>${targetFile.name}(${targetFile.size}Bytes) [${targetFile.type}]</p></div>`;

               const imgTag = addDiv.getElementsByTagName("img")[0];
               const pTag = addDiv.getElementsByTagName("p")[0];

               imagearea.prepend( addDiv );
               const imageURL = URL.createObjectURL(targetFile);

               imgTag.onload = (e)=>{
                   URL.revokeObjectURL(imageURL);
                   pTag.innerHTML = `${pTag.textContent}<br>サイズ:幅 ${imgTag.naturalWidth} 高さ ${imgTag.naturalHeight}`;
                   resolve( true );
               };
               imgTag.onerror = e=>{
                   URL.revokeObjectURL(imageURL);
                   pTag.innerHTML = `${pTag.textContent}<br><span style="color:red">読み込めません</span>`;
                   resolve( false );
               };
               imgTag.src = imageURL;
           });
        };

引数で受け取ったtargetFileは、Fileオブジェクトです。

Fileオブジェクトから、画像データへ文字列でアクセスできる経路を作成します。

画像データへ文字列でアクセスできる経路を作成

const imageURL = URL.createObjectURL(targetFile);

作成した文字列は、ブラウザがURLとして扱うことができます。
そのためimgタグのsrc属性にセットすると、ブラウザが自動的にデータを読み込んでくれます。

imgタグのsrc属性を設定


imgTag.src = imageURL;

src属性をセットする前に、読み込み完了/失敗イベントを登録しておきます。

画像の読み込み完了イベント設定


imgTag.onload = (e)=>{
                   URL.revokeObjectURL(imageURL);
               };
imgTag.onerror = e=>{
                   URL.revokeObjectURL(imageURL);
               };

URL.createObjectURL()を実行すると、画像ファイルデータがメモリ上から削除されなくなります。
imgタグに読み込み後は画像ファイルデータは必要ないので、URL.revokeObjectURL()で削除可能な状態にします。
詳しくは次のページをみてください。
参考記事:【JavaScript】 createObjectURL()した後にrevokeObjectURL()が必要な理由

最後に、loadImageData関数はPromiseオブジェクトを返す関数なので、最後にresolve()またはreject()で結果を通知しています。

Promiseの解決


resolve(true );

この関数の他の処理は、次のような画像リストを形成するタグを作成しています。

画像リストのタグ

<div class="itemarea">
    <div>
        <img >
    </div>
    <div>
        <p></p>
    </div>
</div>

 

画像ファイルをキャンバスに描画

画像ファイルのドロップから直接キャンバスに描画する場合、次のような手順でおこないます。

(1) img要素を作成
(2) img要素のsrc属性を設定
(3) 読み込み完了後、img要素をキャンバスコンテキストのdrawImageに渡して描画

画像ファイルをキャンバスに描画


const imgTag = document.createElement("img");
const imageURL = URL.createObjectURL(targetFile);

imgTag.onload = (e)=>{
                   URL.revokeObjectURL(imageURL);
                   canvasContext.drawImage(imgTag,0,0,400,400);
               };

imgTag.src = imageURL;

 

画像リストクリック処理

今回はドロップされた画像ファイルをリスト表示して、選択されたリストの画像をキャンバスに描画します。

次のitemAreaClick関数は、<div id="imagearea"></div>または、その内部の画像リストがクリックされたときに呼び出されます。

        const itemAreaClick = e =>{
                
            Array.from(imagearea.getElementsByClassName("itemselect"))
                          .forEach( item => item.classList.remove("itemselect"));
            canvasContext.clearRect(0,0,400,400);
 
 
            let itemNode = e.target;
            while( !itemNode.classList.contains("itemarea")){
                if( itemNode.isEqualNode( e.currentTarget) ) return;
                itemNode = itemNode.parentNode;
            }
 
            itemNode.classList.add("itemselect");
            
            const imgTag = itemNode.getElementsByTagName("img")[0];
            canvasContext.drawImage(imgTag,0,0,400,400);
        };

選択状態のリストに"itemselect"クラスがセットされているので、最初にクラスを削除します。

選択状態のクリア


            Array.from(imagearea.getElementsByClassName("itemselect"))
                          .forEach( item => item.classList.remove("itemselect"));

次のクリックされた要素をさかのぼって、"itemarea"クラスを探します。

クリックされた画像リストを取得


            let itemNode = e.target;
            while( !itemNode.classList.contains("itemarea")){
                if( itemNode.isEqualNode( e.currentTarget) ) return;
                itemNode = itemNode.parentNode;
            }

関連記事:【JavaScript】 targetとcurrentTargetの覚書

"itemarea"クラスがみつかったら、"itemselect"を付加して選択状態にします。

選択状態にする


itemNode.classList.add("itemselect");

最後にimgタグを取得して、キャンバスに描画します。

キャンバスに描画


const imgTag = itemNode.getElementsByTagName("img")[0];
canvasContext.drawImage(imgTag,0,0,400,400);

更新日:2020/07/27

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。