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

【JavaScript】 テキストファイルをドラッグ&ドロップして内容を表示する

更新日:2023/01/30

今回はブラウザにテキストファイルをドラッグ&ドロップして、その内容を表示してみます。
その際、UTF-8やシフトJISなどの文字コードが異なるファイルでも読み込めるようにします。

 

仕様/デモ

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

(1) ファイルの内容がテキストのとき、テキストで内容を表示する。その際、文字コードを判別(utf-8やシフトJISなど)して適切に処理します。

(2) ファイルの内容がテキストでないとき、エラーメッセージを表示します。

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

■完成DEMO

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

ここにドロップ

ファイル名:
エンコード:

 

完成コード

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

完成コード:html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>テキストファイルをドラッグ&ドロップして表示する</title>
    <script async src="encoding.js"></script>
    <style>
        body{
            min-height: 100vh;
        }
        #ddarea{
            width:200px;
            height:150px;
            border:1px dotted #888;
            margin: 1em auto;
        }
        #ddarea.ddefect{
            border:1px solid #000;
        }
        #txtarea{
            width: 90%;
            height: 350px;
            overflow: auto;
            background: black;
            color:white;
            white-space: pre;
            padding: 1em;
        }
    </style>
    <script>
    window.addEventListener( "DOMContentLoaded" , ()=> {

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

        let dropDisabled = false;

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

        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) {
                    tarea.textContent = "複数ファイルは対象外です";
                    return;
                }
                dropDisabled = true;

                loadData( e.dataTransfer.files[0] ).then( e => {
                    ddarea.classList.remove("ddefect");
                    dropDisabled = false;
                });
            }
        };

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

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

               fnameSpan.textContent = `${targetFile.name}(${targetFile.size}Bytes)  <${targetFile.type}>`;
               encSpan.textContent = "";
               tarea.textContent ="";

               let encode = null;

               const reader = new FileReader();

               reader.onload = ()=>{

                   const u8 = new Uint8Array(reader.result);

                   const encNames = ["UTF16", "ASCII" , "JIS" , "UTF8" , "EUCJP" , "SJIS", "UNICODE"];

                   const enc =  Encoding.detect(u8);
                   encSpan.textContent = enc;

                   if( encNames.indexOf(enc) < 0 ){
                       tarea.textContent = "ドロップされたファイルはテキストファイルでない可能性があります。";
                   }else{
                       tarea.textContent = Encoding.codeToString(  Encoding.convert(  u8 , "UNICODE"));
                   }

                   resolve(true);
               };
               reader.onerror = () =>{
                   tarea.textContent = reader.error.message;
                   resolve(false);
               };

               reader.readAsArrayBuffer( targetFile );
           });
        };

        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>
<p>ファイル名:<span id="fnameSpan"></span><br>
    エンコード:<span id="encSpan"></span></p>
<div id="txtarea"></div>
<p id="encodejsError"></p>
</body>
</html>

文字コード判定について

今回は文字コードの判定に、encoding.jsというライブラリを使用しています。
encoding.js | GitHub

JavaScriptの標準ライブラリには文字コードを判定できる機能がないので、プログラマ側で作成する必要があります。
しかし非常に面倒です。やってられません。
そんな状況なので、有用なライブラリはとてもありがたいです。

ドラッグ部について

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

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

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

 

ドロップ時の処理

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

ドロップイベントの処理


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

                if(e.dataTransfer.files.length > 1) {
                    tarea.textContent = "複数ファイルは対象外です";
                    return;
                }

                dropDisabled = true;

                loadData( e.dataTransfer.files[0] ).then( e => {
                    ddarea.classList.remove("ddefect");
                    dropDisabled = false;
                });

            }

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

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

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

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

今回は複数ファイルのドラッグを無効にしています。

ドラッグファイルの数が一つのみかチェックしているコード

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

しかしSafariがドラッグ時に複数ファイルチェックできないので、ドロップイベント内でドロップ数のチェックをおこなっています。

 

ファイル読み込みについて

テキストファイル読み込みについて

Fileオブジェクトにはtext()という比較的新しいメソッドがあり、次のようにすることで簡単にテキストファイルを読み込むことができます。

text()メソッドで読み込み


 // text()はPromiseを返す
targetFile.text().then( e=> {
                   tarea.textContent =e;
                   resolve();
               });

しかし読み込めるファイルの文字コードがUTF-8限定なのと、今のところ対応しているブラウザが少ないため、今回のケースでは使用できません。
そこで以前からある方法でテキストファイルを読み込みます。
それはFileReaderオブジェクトを使用した読み込みです。

FileReaderオブジェクトのreadAsText()メソッドを使用すると、テキストファイルを読み込むことができます。

readAsText()での読み込み

const reader = new FileReader();

reader.onload = ()=>{
      // reader.resultに読み込んだ文字列がセットされている
    console.log( reader.result );
};

reader.readAsText( targetFile , "Shift_JIS" );

readAsText()の2番目の引数に文字コードの種類を指定すると、ファイル読み込み時に変換してくれます。
しかし今回の仕様では読み込むまで文字コードがわからないので、この方法は使用できません。

文字コード変換でencoding.jsを使用

今回のテキストファイル読み込みは、文字コードの判定後にJavaScriptで取り扱える文字コードに変更する必要があります。
文字コードの判定は、今回はencoding.jsを使用しています。

encoding.jsは、文字データの配列を引数として受け付けます。
そのため、まずはテキストファイルを配列として読み込みます。

次の手順で処理します。

(1) readAsArrayBuffer()でテキストファイルをarrayBufferオブジェクトに取り込む

(2) Uint8Array()でarrayBufferを配列に変換する。

readAsArrayBuffer()での読み込み


const reader = new FileReader();

reader.onload = ()=>{
      // reader.resultはarrayBuffer
    const u8 = new Uint8Array(reader.result);
};

reader.readAsArrayBuffer( targetFile );

 

ファイル読み込み&表示処理

次のコードは、ファイル読み込みと表示処理をおこなっているloadData関数です。

ファイル読み込み&表示処理部

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

         */
        const loadData = targetFile => {

           return new Promise( (resolve, reject) => {
               fnameSpan.textContent = `${targetFile.name}(${targetFile.size}Bytes)  <${targetFile.type}>`;
               encSpan.textContent = "";
               tarea.textContent ="";

               let encode = null;

               const reader = new FileReader();

               reader.onload = ()=>{

                   const u8 = new Uint8Array(reader.result);

                   const encNames = ["UTF16", "ASCII" , "JIS" , "UTF8" , "EUCJP" , "SJIS", "UNICODE"];

                   const enc =  Encoding.detect(u8);
                   encSpan.textContent = enc;

                   if( encNames.indexOf(enc) < 0 ){
                       tarea.textContent = "ドロップされたファイルはテキストファイルでない可能性があります。";
                   }else{
                       tarea.textContent = Encoding.codeToString(  Encoding.convert(  u8 , "UNICODE"));
                   }

                   resolve(true);
               };
               reader.onerror = () =>{
                   tarea.textContent = reader.error.message;
                   resolve(false);
               };

               reader.readAsArrayBuffer( targetFile );
           });
        };

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

このオブジェクトをFileReaderオブジェクトのreadAsArrayBufferメソッドで読み込み、Uint8Arrayオブジェクトのコンストラクタで文字データの配列を取得します。

Encoding.detect()は文字コードを判定するencoding.jsのメソッドです。
読み込んだファイルは、テキストだけとは限りません。

そこで判定結果から、変換可能かどうかチェックしています。

変換可能かどうかチェック


if( encNames.indexOf(enc) < 0 ){
   // 変換対象ではない
}else{
  // 変換対象
}

配列.indexOf( 値 ) で、配列内で値があるインデックス番号を取得できます。
配列内に存在しない場合は、-1を返します。

テキストファイルであると確認出来たら、Encoding.convert()とEncoding.codeToString()で文字列に変換します。

Encoding.codeToString()

文字データを指定した文字コードに変換。
"UNICODE"は、JavaScriptが内部的に使用している文字コード。

Encoding.convert()

文字データを連結して、文字列に変換する。

文字データを文字列に変換


tarea.textContent = Encoding.codeToString(  Encoding.convert(  u8 , "UNICODE"));

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

Promiseの解決


resolve(true );

encoding.jsは、htmlのhead内で外部スクリプトとして読み込んでいます。

encoding.js読み込み


<script async src="encoding.js"></script>

JavaScript標準の関数ではないので、本来なら正常に読み込まれて使用可能になっているかどうかのチェックが必要です。

encoding.jsが使用可能かチェック


if( window.Encoding === undefined ) { console.error("使用できません"); }

今回は、コードが読みにくくなるためチェック部を除外しています。

更新日:2023/01/30

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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