JavaScript ドラッグ＆ドロップされたテキストファイルの内容を表示する

# はじめに
* [HTML Drag and Drop API](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API) → ドラッグ&ドロップをするために使用  
* [File System Access API](https://developer.mozilla.org/ja/docs/Web/API/FileSystemFileHandle) → ファイルの読み込み、書き込み、管理をするために使用  
* [File API](https://developer.mozilla.org/ja/docs/Web/API/File_API) → ファイルの内容を読み込むために使用  

# `File System Access API`を使用しない
`File API`のみでファイルの内容を読み込みます。

## ファイルが1つの場合

### 通常
まずは外見を作成します。
```html
<!DOCTYPE html>
<html>
<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }
        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script></script>
</head>
<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea" class="textview"></div>
</body>
</html>
```
ページの読み込み時（[DOMContentLoaded](https://developer.mozilla.org/ja/docs/Web/API/Document/DOMContentLoaded_event)）にイベント時の処理を追加します。
* droparea内にファイルが入っているときの処理（[dragover](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/dragover_event)）  
* droparea内にファイルがドロップされたときの処理（[drop](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/drop_event)）  
```javascript
//ページ読み込み時
window.addEventListener("DOMContentLoaded", function () {
    document.getElementById("droparea").addEventListener("dragover", dragover_handler);
    document.getElementById("droparea").addEventListener("drop", drop_handler);
});

//dropareaに入った場合の処理
function dragover_handler(ev) {
    //ここにdropareaに入った場合の処理を記載
}

//dropareaにドロップされた時の処理
function drop_handler(ev) {
    //ここにdropareaにドロップされた時の処理を記載
}
```
ファイルがdropareaに入った場合の処理を作成します。
```javascript
function dragover_handler(ev) {
    //既定の動作を行わない。
    ev.preventDefault();

    //マウスカーソルを+にする。
    ev.dataTransfer.dropEffect = "copy";
}
```
ファイルがdropareaにドロップされた時の処理を作成します。`ev.dataTransfer.files`がドロップされたファイルです。[分割代入](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)で1つ目のファイルを取得します。ファイルが取得出来たら`FileReader`を使ってファイの内容を取得します。
```javascript
function drop_handler(ev) {
    //既定の動作を行わない。
    ev.preventDefault();

    //ドロップされたファイル
    const [file] = ev.dataTransfer.files;

    //FileReaderでテキスト読み込み。
    const reader = new FileReader();
    reader.onload = () => {
        //読み込み完了でファイル名と中身を表示。
        document.getElementById("textarea").innerText = file.name + "\n" + reader.result;
    };
    reader.readAsText(file);
    //reader.readAsText(file, 'shift-jis');//Shift-JISで読み込む場合
}
```
完成
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("load", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }

        //dropareaにドロップされた時の処理
        function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const [file] = ev.dataTransfer.files;

            //FileReaderでテキスト読み込み。
            const reader = new FileReader();
            reader.onload = () => {
                //読み込み完了でファイル名と中身を表示。
                document.getElementById("textarea").innerText = file.name + "\n" + reader.result;
            };
            reader.readAsText(file);
            //reader.readAsText(file, 'shift-jis');//Shift-JISで読み込む場合
        }
    </script>
</head>

<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea" class="textview"></div>
</body>

</html>
```
### asyncを使用
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("DOMContentLoaded", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }

        //dropareaにドロップされた時の処理
        async function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const [file] = ev.dataTransfer.files;

            //ファイル読み込み
            const text = await getTextFromFile(file/*,'shift-jis'*/);

            //表示
            document.getElementById("textarea").innerText = file.name + "\n" + text;
        }

        //ファイルの内容を取得
        function getTextFromFile(file, encoding) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => {
                    resolve(reader.result);
                };
                reader.onerror = () => {
                    reject(new Error());
                };
                reader.readAsText(file, encoding);
            });
        }

    </script>
</head>

<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea" class="textview"></div>
</body>

</html>
```
### CSVを表形式で表示
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        #dataview_cells {
            position: relative;
            overflow-x: auto;
            margin: 0 auto;
            background-color: #e5f1f5;
        }

        .cell {
            position: absolute;
            width: 100px;
            height: 20px;
            font-size: 15px;
            border: 1px solid rgb(97, 100, 102);
            padding: 1px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("DOMContentLoaded", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }

        //dropareaにドロップされた時の処理
        async function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const [file] = ev.dataTransfer.files;

            //ファイル読み込み
            const text = await getTextFromFile(file/*,'shift-jis'*/);

            //テキストを表に変換
            getTableFromText(text);
        }

        //ファイルの内容を取得
        function getTextFromFile(file, encoding) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => {
                    resolve(reader.result);
                };
                reader.onerror = () => {
                    reject(new Error());
                };
                reader.readAsText(file, encoding);
            });
        }

        //テキストを表に変換
        function getTableFromText(text) {

            const h = getElementPropertyPx(".cell", "height");
            const w = getElementPropertyPx(".cell", "width");
            const b = getElementPropertyPx(".cell", "border-width");
            const p = getElementPropertyPx(".cell", "padding");
            let tableheight;

            //データを区切って表に変換
            let inner = "";
            text.replace(/\r\n/g, '\n').split("\n").forEach((line, gyou) => {
                line.split(",").forEach((cell, retu) => {
                    let tate = (h + p * 2 + b) * gyou;
                    let yoko = (w + p * 2 + b) * retu;
                    inner += '<div class="cell" style="top:' + tate + 'px;left:' + yoko + 'px;">' + cell + '</div>';
                    tableheight = (h + p * 2 + b) * (gyou + 2);
                });
            });

            //表示
            document.getElementById("dataview_cells").style.height = String(tableheight) + "px";
            document.getElementById("dataview_cells").innerHTML = inner;
        }

        //要素のスタイル取得
        function getElementPropertyPx(str, pr) {
            let ele = window.getComputedStyle(document.querySelector(str));
            let val = ele.getPropertyValue(pr);
            return Number(val.replace("px", ""));
        }

    </script>
</head>

<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="dataview_cells">
        <div class="cell"></div>
    </div>
</body>

</html>
```
## ファイルが複数の場合
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("DOMContentLoaded", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }
        let counter = -1;
        //dropareaにドロップした時の処理。
        function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const files = [...ev.dataTransfer.files];

            //ファイルの数だけ表示エリア作成
            let current_counter = counter;
            files.forEach(x => {
                counter++;
                document.getElementById("textareachild").outerHTML =
                    `<div id="textareachild${counter}" class="textview"></div>
                    <div id="textareachild"></div>`;
            });

            //ファイルの数だけ繰り返し
            files.forEach((file, i) => {
                //FileReaderでテキスト読み込み。
                const reader = new FileReader();
                reader.onload = () => {
                    //読み込み完了でファイル名と中身を表示。
                    document.getElementById(`textareachild${current_counter + i + 1}`).innerText = file.name + "\n" + reader.result;
                };
                reader.readAsText(file);
            });
        }
    </script>
</head>

<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea">
        <div id="textareachild"></div>
    </div>
</body>

</html>
```
# `File System Access API`を使用する
`File System Access API`の`FileSystemHandle`インターフェースを使用すると、ブラウザからローカルファイルの読み書きなどが一般的なソフトウェアのようにできるため非常に強力です。  

`FileSystemHandle`の取得はまだ実験的な機能であり対応しているブラウザも限られます。  
* [getAsFileSystemHandle()](https://developer.mozilla.org/ja/docs/Web/API/DataTransferItem/getAsFileSystemHandle)  
* [showOpenFilePicker()](https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker)  
* [showDirectoryPicker()](https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker)  
* [showSaveFilePicker()](https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker)  

### 通常
`FileSystemHandle`を取得したうえで、`File API`でファイルの内容を読み込みます。
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("DOMContentLoaded", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }
        //dropareaにドロップした時の処理。
        async function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const [item] = [...ev.dataTransfer.items].filter(item => item.kind === 'file');

            //ファイルハンドル取得
            handle = await item.getAsFileSystemHandle();

            //ファイル取得
            const file = await handle.getFile();

            //ファイル内容取得
            const text = await getTextFromFile(file/*, 'shift-jis'*/);

            //表示
            document.getElementById("textarea").innerText = text;
        }

        //ファイルの内容を取得
        function getTextFromFile(file, encoding) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => {
                    resolve(reader.result);
                };
                reader.onerror = () => {
                    reject();
                };
                reader.readAsText(file, encoding);
            });
        }
    </script>
</head>


<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea" class="textview"></div>
</body>

</html>
```
### リアルタイムで表示を更新
ファイルが更新されると自動で表示内容を更新します。
```html
<!DOCTYPE html>
<html>

<head>
    <style>
        #droparea {
            background-color: #e9f7c4;
        }

        .textview {
            background-color: #e5f1f5;
            margin-top: 10px;
        }
    </style>
    <script>
        //ページ読み込み時
        window.addEventListener("DOMContentLoaded", function () {
            document.getElementById("droparea").addEventListener("dragover", dragover_handler);
            document.getElementById("droparea").addEventListener("drop", drop_handler);
            setInterval(realtimePreview, 1000);
        });

        //dropareaに入った場合の処理
        function dragover_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //マウスカーソルを+にする。
            ev.dataTransfer.dropEffect = "copy";
        }
        //dropareaにドロップした時の処理。
        async function drop_handler(ev) {
            //既定の動作を行わない。
            ev.preventDefault();

            //ドロップされたファイル
            const [item] = [...ev.dataTransfer.items].filter(item => item.kind === 'file');

            //ファイルハンドル取得
            handle = await item.getAsFileSystemHandle();

            //状態リセット
            date = undefined;
            processing = finished = false;
        }

        //常時更新表示
        let handle;
        let processing = false;
        let finished = false;
        async function realtimePreview() {
            if (handle !== undefined && !processing && !finished) {
                processing = true;
                try {
                    const file = await handle.getFile();
                    const text = await getTextFromFile(file/*, 'shift-jis'*/);
                    if (typeof (text) == "string") {
                        document.getElementById("textarea").innerText = text;
                    }
                } catch (e) {
                    finished = true;
                }
                processing = false;
            }
        }

        //ファイルの内容を取得
        let date;
        function getTextFromFile(file, encoding) {
            if (date == undefined || file.lastModified > date) {
                date = file.lastModified;
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.onload = () => {
                        resolve(reader.result);
                    };
                    reader.onerror = () => {
                        reject();
                    };
                    reader.readAsText(file, encoding);
                });
            } else {
                return Promise.resolve(0);
            }
        }
    </script>
</head>


<body>
    <div id="droparea">ここにテキストファイルをドロップ</div>
    <div id="textarea" class="textview"></div>
</body>

</html>
```