MENU

JavaScriptドラッグ&ドロップaddeventlistenerDOMファイル操作

【JavaScript】 ファイルをドラッグ&ドロップの基本形

更新日:2020/07/27

 

ファイルのドロップ機能を実装

 

次のような条件で、ドロップ機能を実装します。

 

(1) ドロップエリアにファイルがドラッグされたら、エリアの外観を変更する。

 

ドラッグ スタイル変更

 

(2) ドロップエリア外にファイルがドラッグされたら、ドロップを無効にする。

 

ドラッグ 無効にする

 

(3) ドロップエリアにファイルがドロップされたら、テキストエリアにファイルのリストを表示する。

 

 

■完成DEMO

 

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

 

ここにドロップ

 

完成コード

 

今回は、html内にスクリプトとスタイルを含めています。

 

完成コード:html

 

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ファイルをドラッグ&ドロップして名前のリストを受け取る</title>
    <style>
        body{
            min-height: 100vh;
        }
        #ddarea{
            width:200px;
            height:150px;
            border:1px dotted #888;
            margin: 1em auto;
        }
        #ddarea.ddefect{
            border:1px solid #000;
        }
        #txtarea{
            padding:5px;
            width: 90%;
            height: 150px;
            overflow: auto;
            background: black;
            color:white;
            margin: 1em auto;
        }
        .txtareawrap{
            text-align:center;
            margin-bottom:1em;
        }
    </style>
    <script>
    window.addEventListener( "DOMContentLoaded" , ()=> {

        const ddarea = document.getElementById("ddarea");
        const tarea = document.getElementById("txtarea");

            // ドラッグされたデータが有効かどうかチェック
        const isValid = e => e.dataTransfer.types.indexOf("Files") >= 0;

        const ddEvent = {
            "dragover" : e=>{
                e.preventDefault(); // 既定の処理をさせない
                if( !e.currentTarget.isEqualNode( ddarea ) ) {
                        // ドロップエリア外ならドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                e.stopPropagation(); // イベント伝播を止める

                if( !isValid(e) ){
                        // 無効なデータがドラッグされたらドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                        // ドロップのタイプを変更
                e.dataTransfer.dropEffect = "copy";
                ddarea.classList.add("ddefect");
            },
            "dragleave" : e=>{
                if( !e.currentTarget.isEqualNode( ddarea ) ) {
                    return;
                }
                e.stopPropagation(); // イベント伝播を止める
                ddarea.classList.remove("ddefect");
            },
            "drop":e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める

                const files = e.dataTransfer.files;

               tarea.value +=`${files.length}のファイルがドロップされた。`;
                for( file of files ) tarea.value += `name:${file.name} type:${file.type}` ;

                ddarea.classList.remove("ddefect");
            }
        };

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

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

<div id="ddarea" ><p>ここにドロップ</p></div>
<div class="txtareawrap">
<textarea id="txtarea"></textarea>
</div>
</body>
</html>

 

div#ddareaがドロップを受け付けるエリアです。

 

ドロップエリアの外観は、クラスの追加/削除で切り替えます。

 

クライアント

 

また、このスクリプトはdiv#ddarea内に他の要素が挿入されても動作します。

 

次のように、ドロップエリアをdiv要素で覆ってしまっても問題ありません。

 

<div id="ddarea" ><div style="width:100%;height:100%" ></div></div>

 

 

イベントの登録

 

まずはhtmlがブラウザで読み込まれ、DOMが構築されるのを待ちます。
JavaScriptでDOM操作をおこなうときのお約束ですね。

 

DOM構築を待つ

 


window.addEventListener( "DOMContentLoaded" , ()=> {

});

 

 

次に、ドラッグイベントなどを要素に登録していきます。

 

今回は二つの要素に、同じコールバック関数を登録しているので、一度オブジェクトで定義しています。

 

イベントリスナーの登録

 


        const ddEvent = {
            "dragover" : e=>{  /* 処理 */ },
            "dragleave" : e=>{ /* 処理 */ },
            "drop":e=>{ /* 処理 */}
        };

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

 

今回はドロップエリアとbody要素にイベントを登録しています。

 

addEventListenerの第三引数を指定していないので、ドロップエリア→body要素の順でイベントが通知されます。
この順番にすることで、ドロップエリア以外でドラッグしたときにドロップを無効化することが可能になります。

 

addEventListenerのイベント順序については、次の記事で解説しています。
参考記事:【JavaScript】 addEventListener()の第三引数useCaptureの謎

 

dragoverイベント

 

次はドラッグを捕捉するイベントです。

 

dragoverイベント

 


            "dragover" : e=>{
                e.preventDefault(); // 既定の処理をさせない
                if( !e.currentTarget.isEqualNode( ddarea ) ) {
                        // ドロップエリア外ならドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                e.stopPropagation(); // イベント伝播を止める

                if( !isValid(e) ){
                        // 無効なデータがドラッグされたらドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                        // ドロップのタイプを変更
                e.dataTransfer.dropEffect = "copy";
                ddarea.classList.add("ddefect");
            },

 

既定の処理を無効化

 

関数が呼び出されたら、preventDefault()で既定の処理を無効化します。

 

この記事を書いている時点(2020/7)では、preventDefault()がなくてもfirefoxは動作しました。
しかしchromeは動作しなかったので、preventDefault()は必須です。

 

ドロップエリアかどうかの確認

 

次に今処理している要素が、ドロップエリアかどうかを確認します。
currentTargetが今処理している要素なので、この要素のisEqualNode()メソッドに、別で取得しておいたドロップエリアのインスタンスを渡して同一チェックをおこなっています。

 

一致しなければ、dataTransfer.dropEffectに"none"をセットして、ドロップ機能を無効にします。

 

イベントの伝播をストップ

 

この時点で、今処理しているのがドロップエリアと確定しました。
そこで、イベントの伝播をstopPropagation()メソッドでストップします。
ストップしないと、body要素にイベント通知され、ドロップ機能を無効になるので、とても重要な処理です。

 

有効なデータかをチェック

 

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

 

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

 

dataTransfer.typesは配列で、ファイルがドロップされると要素の一つに"Files"がセットされるので、indexOf()で確認しています。

 

ちなみに次のコードに変更すると、ドロップファイルが複数のときに、ドロップを無効化できます。

 

const isValid = e => e.dataTransfer.types.indexOf("Files") >= 0 && e.dataTransfer.items.length<=1;

 

 

dataTransferオブジェクトは、filesとitemsというプロパティを持っています。

 

filesはドロップされたファイルを管理するFileオブジェクトのリストです。ファイル名やサイズ、ファイルの読み込みなどに使用します。

 

itemsはドラッグ中またはドロップしたデータを管理するためのDataTransferItemオブジェクトのリストです。ファイルのドロップ操作の場合、このプロパティは使用しません。

 

■e.dataTransfer.items.length<=1の理由

 

2020/6現在のFirefoxとChromeで確認したところ、ドラッグ中において、filesプロパティにFileオブジェクトがセットされていませんでした。
(ドロップ後のfilesプロパティにはセットされています)

 

そこでitemsプロパティはファイル操作で使用しないのですが、ドラッグファイル数確認のために使用しています。

 

しかしmacのsafariでは、ドラッグ中のfilesとitems共にデータ数がゼロでした。
そのため、ドラッグ中のファイル数が一つのみかという確認に対して、1以下(0も含める)という条件にしてあります。

 

 

ドロップのタイプを変更

 

次にドロップのタイプを変更します。

 

e.dataTransfer.dropEffect = "copy";

 

"copy"の他に"move"や"link"などを指定できますが、ここでは"copy"が妥当と考えてこの値にしてあります。

 

外観変更用クラスをセット

 

最後に、ドロップエリアにddefectクラスをセットして終了です。

 

 

dragleaveイベント

 

次はドロップエリアからマウスが出たときのイベント処理です。

 

dragleaveイベント

 


            "dragleave" : e=>{
                if( !e.currentTarget.isEqualNode( ddarea ) ) {
                    return;
                }
                e.stopPropagation(); // イベント伝播を止める
                ddarea.classList.remove("ddefect");
            },

 

イベントを受け取った要素がドロップエリアだったら、イベントの伝播と止めて、ddefectクラスを削除しています。

dropイベント

 

次はマウスから指が離されて、ファイルがドロップされたときのイベント処理です。

 

dropイベント

 


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

                const files = e.dataTransfer.files;

                tarea.value += `${files.length}のファイルがドロップされた。`;
                for( file of files ) tarea.value += `name:${file.name} type:${file.type}` ;

                ddarea.classList.remove("ddefect");
            }

 

dropイベントは、dataTransfer.dropEffectに"none"をセットすると、発生しません。
dragoverイベントで、ドロップエリア以外は"none"をセットしているので、ここではドロップされた要素のチェックをおこなっていません。

 

最初にpreventDefault()で既定の処理をおこなっています。
この処理をおこなわないと、ドロップしたファイルをブラウザで開いてしまうので、重要です。

 

次にstopPropagation()でイベントの伝播を止めます。

 

次にdataTransfer.filesオブジェクトからファイル名を取得して、テキストエリアに出力します。

 

dataTransfer.filesオブジェクトは、Fileオブジェクトをインデックス番号で格納しています。

 

例:
dataTransfer.files[0] → 一つ目のFileオブジェクト
dataTransfer.files[1] → 二つ目のFileオブジェクト

 

Fileオブジェクトは、ファイル名などの情報を持っています。

Fileオブジェクト{ name: ファイル名(パスは含まない) lastModified : 更新時刻(1970/1/1 0:00からの経過ミリ秒) size : ファイルサイズ(バイト数) type : MIMEタイプ(ブラウザによって異なる。判定できないときは"") }

最後にddefectクラスを削除して、ドロップエリアのエフェクトを解除します。

 

考察

 

ドロップエリア内に何も要素を置かないことを前提として、イベントの効率を上げることができます。

 

次のコードは、最初に紹介したhtmlコードのJavaScript部を抜き出し、書き換えたものです。

 

 

イベント効率化:JavaScript部のみ

 


    window.addEventListener( "DOMContentLoaded" , ()=> {

        const ddarea = document.getElementById("ddarea");
        const tarea = document.getElementById("txtarea");

            // ドラッグされたデータが有効かどうかチェック
        const isValid = e => e.dataTransfer.types.indexOf("Files") >= 0;

        const ddEvent = {
            "dragover" : e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める
                if( !e.target.isEqualNode( ddarea ) ) {
                        // ドロップエリア外ならドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                // e.stopPropagation(); // イベント伝播を止める

                if( !isValid(e) ){
                        // 無効なデータがドラッグされたらドロップを無効にする
                    e.dataTransfer.dropEffect = "none";return;
                }
                        // ドロップのタイプを変更
                e.dataTransfer.dropEffect = "copy";
                ddarea.classList.add("ddefect");
            },
            "dragleave" : e=>{
                e.stopPropagation(); // イベント伝播を止める
                if( !e.target.isEqualNode( ddarea ) ) {
                    return;
                }
                // e.stopPropagation(); // イベント伝播を止める
                ddarea.classList.remove("ddefect");
            },
            "drop":e=>{
                e.preventDefault(); // 既定の処理をさせない
                e.stopPropagation(); // イベント伝播を止める

                const files = e.dataTransfer.files;

                tarea.value += `${files.length}のファイルがドロップされた。`;
                for( file of files )tarea.value +=  `name:${file.name} type:${file.type}`;

                ddarea.classList.remove("ddefect");
            }
        };

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

    });

 

赤文字が、変更箇所です。

 

ドロップエリアのイベント登録を削除したため、body要素のみでイベントを捕捉します。
また、addEventListenerの第三引数をtrueにしているので、キャプチャフェーズでイベントが発生します。

 

イベントは次の順番で伝播します。

 

window→document→html→body→・・・→div→・・・→body→html→document→window

 

赤文字がキャプチャフェーズで、緑がバブリングフェーズです。

 

今回は赤文字のbodyでイベントを捕捉するので、かなり早い段階で処理をおこなっていることになります。
さらに、この時点で伝播を停止するので、処理が効率化されていると考えられます。

 

各イベントでは、オブジェクトを引数として受け取ります。
このオブジェクトは、currentTargetプロパティとtargetプロパティを持っています。

 

currentTargetは、イベントを受け取った要素、つまりbodyです。

 

targetは、イベントが発生した要素です。

 

つまりtargetとドロップエリアのインスタンスが一致するかどうかをチェックして、処理の内容を決定します。

 

ただしドロップエリア内に別の要素(pやdivなど)があると、その要素がtargetとなります。
そのためこの方法は、ドロップエリア内が空であることが条件です。

記事の内容について

 

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


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

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

そんなときは、ご意見もらえたら嬉しいです。

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