HTML&JavaScript - Drag&Dropでファイルを読み取るまでの解説とサンプル（バイナリビューワ）

※HTML/JavaScript/CSSは仕事では使ってないので、変なところあるかもです。まずいところあればご指摘ください。

# ファイルのDrag&Dropからデータの読み込みまでの流れ

## 1. HTMLの要素に対してイベント処理を登録する
HTML上の特定要素（ここでは`<div id="dropZone">`）に対して、下記のようにイベント処理を登録[^1]する。

[^1]: `addEventListener`を使ってイベント処理を登録してもよい。

```js:JavaScript

const dropZone = document.getElementById('dropZone');

dropZone.ondrop = function(evt) {
    /* ドロップされたときに実行する処理 */
}
dropZone.ondragover = function(evt) {
    /* ドラッグされたときのお決まりの処理。（いわゆる、おまじない）
       本文中の「ソースコード」章を参照ください。 */
}
```

[Drag&Drop関連イベントの詳細](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API)

## 2. ドロップされたときにファイルのハンドル（？）を受け取る

[`ondrop`イベント](https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/ondrop)で受け取れる[引数(ここでは`evt`)](https://developer.mozilla.org/ja/docs/Web/API/DragEvent)が、ファイルをハンドルするための[`dataTransfer.files`](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer/files)をもっており、これを処理します。
`files`という名前の通り、複数のファイルがドロップされる場合もあります。

```js:JavaScript

dropZone.ondrop = function(evt) {
    /* 中略 */
    let files = evt.dataTransfer.files; /* ★Dropされたファイルたちのハンドル */
    /* 中略 */
    let f = files[0]; /* １つ目のファイル */
    /* 中略 */
}
```

上記の`f`からは、[ファイル名(`name`)やファイルサイズ(`size`)などの情報を得られます。](https://developer.mozilla.org/ja/docs/Web/API/File)

## 3. ファイルハンドルからファイルの中身を読み出す

[ファイルの中身を読み出すには、`FileReader`を使います。](https://developer.mozilla.org/ja/docs/Web/API/FileReader)
ファイルハンドル(ここでは`f`)に対して[`readAsArrayBuffer(f)`](https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsArrayBuffer)を実行すると、読み出しが始まります。
※この`readAsArrayBuffer`による読み出し処理は非同期処理です。呼出し処理以下に記載された処理はそのままブロックされずに実行されます。

読み出しが完了すると[`onload`](https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload)イベントが発生します。このイベントの[引数(ここでは`evt`)](https://developer.mozilla.org/ja/docs/Web/API/ProgressEvent)に含まれる[`target.result`が読み出し結果のデータにあたります。](https://developer.mozilla.org/ja/docs/Web/API/FileReader/result)
ただし、このままだと[ArrayBuffer](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)という扱いづらい型になっているので、[適宜byte配列などに変換したほうがよいでしょう。](https://qiita.com/yaegaki/items/909587a2dae20467c74a)(下記コードでは[`new Uint8Array`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)を使用して変換しています。)

```js:JavaScript

const fileReader = new FileReader(); /* ★FileReaderの実体を1つ用意する */

dropZone.ondrop = function(evt) {
    /* 中略 */
    let files = evt.dataTransfer.files; /* 一旦変数に代入 */
    /* 中略 */
    let f = files[0]; /* １つ目のファイル */
    /* 中略 */
    fileReader.readAsArrayBuffer(f); /* ★ファイルの読み込みを開始 */
    /* 中略 */
}

fileReader.onload = function(evt) {
    /* 読み込みが完了すると呼び出される */
    const content = new Uint8Array(evt.target.result); /* ★読み出し結果をbyte配列に変換 */
    /* 中略 */
}

fileReader.onprogress = function(evt) {
    /* 読み込み中に呼び出される */
}
```

あとはただのbyte配列データなので煮るなり焼くなりできるかと思います。

# サンプル

<p class="codepen" data-height="400" data-theme-id="light" data-default-tab="result" data-user="kob58im" data-slug-hash="PoNgmjM" style="height: 400px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="BinaryFileViewer(File Drag&amp;amp;Drop Sample)">
  <span>See the Pen <a href="https://codepen.io/kob58im/pen/PoNgmjM">
  BinaryFileViewer(File Drag&amp;Drop Sample)</a> by kob58im (<a href="https://codepen.io/kob58im">@kob58im</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

※ハングアップ等回避のため、10KB以下のファイルしか受け取らないように処理を入れてあります。


# ソースコード

```js:JavaScript

const dropZone = document.getElementById('dropZone');
const progBar = document.getElementById('progBar');
const outFileInfo = document.getElementById('outFileInfo');
const outContent = document.getElementById('outContent');
const fileReader = new FileReader();
const maxFileSize = 10*1024; //bytes
const colN = 16;

dropZone.ondrop = function(evt)
{
    evt.stopPropagation();
    evt.preventDefault();

    let files = evt.dataTransfer.files; // FileList object.
    if ( files.length === 1 ) {
        let f = files[0];
        if ( f.size <= maxFileSize ) {
            outFileInfo.innerText = f.name+'\n'+f.size+'bytes';
            fileReader.readAsArrayBuffer(f);
        }
    }
}

dropZone.ondragover = function(evt)
{
    evt.stopPropagation();
    evt.preventDefault();
    evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
}

fileReader.onload = function(evt)
{
    progBar.value = 100;
    
    const content = new Uint8Array(evt.target.result);
    generateTable(content);
}


fileReader.onprogress = function(evt)
{
    let per;
    if ( evt.total === 0 ) {
        per = 0;
    }
    else {
        per = (evt.loaded/evt.total) * 100;
    }
    progBar.value = per;
}
function generateTable(content)
{
    let output = [];
    const rowN = Math.ceil(content.length/16);
    
    output.push('<table><tr><th class="fixed01"></th>');
    for ( let col=0; col<colN; col++ ) {
        output.push('<th class="fixed02">+', col.toString(16), '</th>');
    }
    output.push('</tr>');
    for ( let row=0; row<rowN; row++ ) {
        output.push('<tr>');
        output.push('<th class="fixed02" align="right">',(row*colN).toString(16),':</th>');
        for ( let col=0; col<colN; col++ ) {
            let i = row * colN + col;
            if ( i < content.length ) {
                output.push('<td>', ('0'+content[i].toString(16)).slice(-2), '</td>');
            }
            else {
                output.push('<td></td>');
            }
        }
        output.push('</tr>');
    }
    output.push('</table>');
    outContent.innerHTML = output.join('');
}


function generatePreviewTable()
{
    const content = new Uint8Array(50);
    // make a random like sequence (don't use for technical usage)
    for(let i=0;i<content.length;i++){
        content[i] = (37*i+(i%17)+79)%256; // these value have no reason nor meaning.
    }
    generateTable(content);
}

generatePreviewTable();
```

```html:HTML

<html>
  <body bgcolor="black" text="lightgray">
    <div id="dropZone">
      Drop a file.<br>(Note: this sample code checks file size to reject a file over 10240 bytes)<br>
        This sample code does not intend to transfer dropped file data to network. <br>But I strongly recommend you <u>should NOT drop file such as copyrighted nor confidential nor personal information etc... to browser</u> for security reason.<br>
      <progress id="progBar" max="100" value="0" style="width: 80px; "></progress>
      <output id="outFileInfo"></output><br>
      <output id="outContent"></output>
    </div>
  </body>
</html>
```


```css:CSS

div {
  border: 2px solid #999999;
}
table {
  width: 100%
  font-size: 50%;
  font-family:monospace;
}
th,td{
  vertical-align: middle;
  padding: 0 7px;
  border: 1px solid #ccc;
}

/* to fix header column and header row for scrolling */
/* thanks to  https://since-inc.jp/blog/8675 */
.fixed01,
.fixed02{
  position: sticky;
  top: 0;
  left: 0;
  color: #66c;
  background: #333;
  &:before{
    content: "";
    position: absolute;
    top: -1px;
    left: -1px;
    width: 100%;
    height: 100%;
    border: 1px solid #ccc;
  }
}
.fixed01{
  z-index: 2;
}
.fixed02{
  z-index: 1;
}
```

# 参考サイト

- [File の Drag & Drop 読み込み 参考サイト](https://qiita.com/kob58im/items/80971a854efa92871fa9#file-%E3%81%AE-drag--drop-%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF)
- [CSSでテーブル表の一部を固定してスクロールする方法 - シンス](https://since-inc.jp/blog/8675)