【Javascript】フレームワークを使わずにドラッグアンドドロップでjsonを読み込む

# TL;DR

[記事末のソースを見てね！](#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%81%BE%E3%81%A8%E3%82%81)

## やりたいこと

Github Pagesで、静的ページだけどドラッグアンドドロップでjsonファイルを読み込みたかった。

## ポイント

### 1. ブラウザデフォルトのドラッグアンドドロップ関係のイベント処理を止める

ブラウザにファイルをドラッグアンドドロップすると、ドロップしたファイルを読み込んでブラウザの機能としてプレビューする、またはダウンロードしてしまいます。まずはこれを止めます。

```js:Javascript
const drop_area = document.getElementById("drop_area");

// リストのイベントすべてに同じ処理をセットする
["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave"].forEach(event => {
  drop_area.addEventListener(event, (e) => {
    // ブラウザデフォルトのイベント処理を停止
    e.preventDefault();
    e.stopPropagation();
  });
});
```

### 2. ドロップイベントがあったとき非同期通信でGETする

Github PagesはPOSTメソッドを受け付けていないので（ステータスコード405が帰ってくる）、GETメソッドで行きます。

```js:Javascript
function handleDropAction(e) {
  // ブラウザデフォルトのイベント処理を停止
  e.preventDefault();
  e.stopPropagation();

  const droppedFiles = e.dataTransfer.files; // ドロップしたファイルを取得

  // ファイルが一つ以上ドロップされたら
  if (droppedFiles.length > 0) {
    const ajaxData = new FormData();
    const ajax = new XMLHttpRequest();
    const file = droppedFiles[0]; // 2つ目以降のファイルを今回は無視する

    ajaxData.append("file", file); // 非同期通信で送信するデータにドロップしたファイル情報を追記
    ajax.open("GET", "index.html");  // 非同期通信の準備。普通のサーバならPOSTでも可
    
    // 送信が終わったときの処理
    ajax.onload = (e) => {
      if (ajax.status >= 200 && ajax.status < 400) { // 送信に成功したら読み込む
        const fr = new FileReader();

        // ファイルの読み込みが終わったときの処理
        fr.onload = () => {
          document.getElementById("viewer").innerText = fr.result;
        };
        fr.readAsText(file); // ドロップしたファイルをテキストとして読み込み
      }
    }
    ajax.send(ajaxData); // ドロップしたファイルを表示中のページに送信する
  }
}

// あらかじめ定義しておく
// const drop_area = document.getElementById("drop_area");

// id="drop_area"要素にドロップしたとき呼び出す関数を関連付け
drop_area.addEventListener("drop", handleDropAction); 
```

## 注意

外部ファイルの読み込みになるので、`file://`で表されるURLではオリジン間リソース共有([CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS))扱いになってエラーが出て、うまく処理されません。

ローカルで試す場合は、PHPやPython、Rubyなどで簡易的にHTTPサーバを立てるとよいでしょう。

```sh:shell
# PHPでHTTPサーバを立てる例
cd /path/to/src/ # index.html のあるディレクトリに移動しておく
php -S localhost:8080  # http://localhost:8080/ にアクセスするとうまく動く
```

## ソースコードまとめ

### XMLHttpRequest版

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sample</title>
</head>
<body>
  <div id="drop_area" style="width:100%; height:100%;">drop area</div>
  <div id="viewer"></div>
</body>
<script>
  function handleDropAction(e) {
    // ブラウザデフォルトのイベント処理を停止
    e.preventDefault();
    e.stopPropagation();

    const droppedFiles = e.dataTransfer.files; // the files that were dropped

    if (droppedFiles.length > 0) {
      const ajaxData = new FormData();
      const ajax = new XMLHttpRequest();
      const file = droppedFiles[0];

      ajaxData.append("file", file);
      ajax.open("GET", "index.html");  // POSTでも可
      ajax.onload = (e) => {
        if (ajax.status >= 200 && ajax.status < 400) {
          const fr = new FileReader();
          fr.onload = () => {
            document.getElementById("viewer").innerText = fr.result;
          };
          fr.readAsText(file);
        }
      }
      ajax.send(ajaxData);    
    }
  }

  const drop_area = document.getElementById("drop_area");
  ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave"].forEach(event => {
    drop_area.addEventListener(event, (e) => {
      // ブラウザデフォルトのイベント処理を停止
      e.preventDefault();
      e.stopPropagation();
    });
  });
  drop_area.addEventListener("drop", handleDropAction);
</script>
</html>
```

### fetch版

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sample</title>
</head>
<body>
  <div id="drop_area" style="width:100%; height:100%;">drop area</div>
  <div id="viewer"></div>
</body>
<script>
  function handleDropAction(e) {
    // ブラウザデフォルトのイベント処理を停止
    e.preventDefault();
    e.stopPropagation();

    const droppedFiles = e.dataTransfer.files; // the files that were dropped

    if (droppedFiles.length > 0) {
      const ajaxData = new FormData();
      const file = droppedFiles[0];

      ajaxData.append("file", file);
      fetch("index.html", {

      })
      .then(response => {
        const fr = new FileReader();
        fr.onload = () => {
          document.getElementById("viewer").innerText = fr.result;
        };
        fr.readAsText(file);
      })
      .catch(err => {
        document.getElementById("viewer").innerText = JSON.stringify(err);
      });
    }
  }

  const drop_area = document.getElementById("drop_area");
  ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave"].forEach(event => {
    drop_area.addEventListener(event, (e) => {
      // ブラウザデフォルトのイベント処理を停止
      e.preventDefault();
      e.stopPropagation();
    });
  });
  drop_area.addEventListener("drop", handleDropAction);
</script>
</html>
```