【JavaScript入門】ドラック&ドロップとファイルの読み込む(imageArea.js)

 ここでは 画像アップロード UI の核となる imageArea.js を実装し、ドラッグ&ドロップ/ファイルダイアログ/File Reader を組み合わせて「画像 ⇒ Canvas ⇒ localStorage」までを完結させる流れを解説します。あわせて、前回提示した storage.js の主な API も再掲し、どこで連携しているかを整理します。

フォルダ構成

node-js/
 ├─ app/
 │   ├─ index.html
 │   ├─ style.css
 │   └─ js/
 │        ├─ common.js
 │        ├─ storage.js        ← 画像&コメントの永続化
 │        ├─ imageArea.js      ← ★今回実装
 │        ├─ imageEffects.js
 │        ├─ controlArea.js
 │        ├─ commentArea.js
 │        ├─ sendArea.js
 │        ├─ dateFormat.js
 │        └─ postListArea.js
 ├─ index.js                    ← API/サーバ側(今回触れず)
 ├─ upload_images/
 └─ post-dat.txt

imageArea.js ― フルコード & 詳細コメント

/* ---------------------------------------------------
   imageArea.js
   - #imageArea で画像ファイルを受け取り Canvas に描画
   - 受け取り方法は ①ドラッグ&ドロップ ②クリック→ファイルダイアログ
   - 描画完了後は storage.js の saveCanvasToStorage() を呼び
     次回アクセス時に自動復元できるようにする
--------------------------------------------------- */
window.addEventListener('DOMContentLoaded', () => {
  const dropZone   = document.querySelector('#imageArea');
  const hiddenFile = document.querySelector('#file');
  const canvas     = document.querySelector('#view');
  const ctx        = canvas.getContext('2d');
  const noView     = document.querySelector('#noView');

  /* === 1. ドラッグオーバー時のハイライト === */
  ['dragenter', 'dragover'].forEach(ev =>
    dropZone.addEventListener(ev, e => {
      e.preventDefault();              // デフォルト挙動(ブラウザ遷移)を阻止
      dropZone.classList.add('isDrag'); // CSS で枠色変更など
    }));

  /* === 2. ドラッグアウト時にハイライト解除 === */
  ['dragleave', 'drop'].forEach(ev =>
    dropZone.addEventListener(ev, () => dropZone.classList.remove('isDrag')));

  /* === 3. ファイルをドロップした瞬間 === */
  dropZone.addEventListener('drop', async e => {
    e.preventDefault();
    if (!e.dataTransfer.files.length) return;
    const file = e.dataTransfer.files[0];
    await drawImageFile(file);
  });

  /* === 4. クリックで隠し <input type=file> を起動 === */
  dropZone.addEventListener('click', () => hiddenFile.click());

  /* === 5. input でファイル選択された場合 === */
  hiddenFile.addEventListener('change', async () => {
    if (!hiddenFile.files.length) return;
    const file = hiddenFile.files[0];
    await drawImageFile(file);
    hiddenFile.value = '';             // 同じファイルでも change が発火するようリセット
  });

  /* -------------------------------------------------
     drawImageFile()
     指定 File → Base64 取得 → <img> → Canvas 描画
  ------------------------------------------------- */
  async function drawImageFile(file){
    if (!file.type.startsWith('image/')){
      alert('画像ファイルを指定してください');
      return;
    }
    try{
      // FileReader を Promise ラップで DataURL 取得
      const dataURL = await readAsDataURL(file);

      // <img> に読み込んでから Canvas へ描画
      await new Promise((res, rej) => {
        const img = new Image();
        img.onload = () => res(img);
        img.onerror= () => rej(new Error('画像の読み込みに失敗しました'));
        img.src = dataURL;
      }).then(img => {
        // Canvas のサイズをアップロード画像の比率で調整したい場合はここで
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        canvas.style.display = 'block';
        noView.style.display = 'none';
        canvas.setAttribute('time', Date.now());
      });

      // 永続化 (storage.js)
      saveCanvasToStorage();

    }catch(err){
      console.error(err);
      alert('ファイルの読み込みに失敗しました');
    }
  }

  /* -------------------------------------------------
     readAsDataURL() : File → Base64 を Promise 化
  ------------------------------------------------- */
  function readAsDataURL(file){
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload  = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsDataURL(file);        // ★ここが本題
    });
  }
});

imageArea.js の主要 API/イベント

要素/API働き具体的な使い方
dragenter / dragoverドロップ可能領域への侵入/滞留e.preventDefault() でブラウザ既定動作を抑止
dropドロップ完了e.dataTransfer.files[0] で File を取得
FileReader.readAsDataURL(file)File → Base64画像プレビューに最適
<input type="file">手動選択ダイアログ.click() でプログラム的に発火
CanvasRenderingContext2D.drawImage()画像描画ctx.drawImage(img, x, y, w, h)
saveCanvasToStorage()Canvas を localStorage に永続化storage.js で定義済み

storage.js との連携ポイント

imageArea.js 内の処理呼び出す storage.js API目的
画像描画完了後saveCanvasToStorage()次回アクセス時に同じ画像を自動復元
“削除” ボタン(controlArea.js 側)removeCanvasStorage()localStorage から画像データを消去

storage.js についての詳細な関数一覧や内部動作は 前回記事 を参照してください。

関数別フロー解説

  1. DOMContentLoaded
    ・すべてのイベントリスナを設定。これ以降は宣言的に待つだけ。
  2. ドラッグオーバー/リーブ
    ・視覚フィードバックとして isDrag クラスを付与・除去。Shadow/Border 変更は CSS で。
  3. drop / change
    ・いずれも drawImageFile(file) へ File オブジェクトを渡す一点に集約。
  4. drawImageFile(file)
    型チェックreadAsDataURLImage オブジェクト化Canvas 描画saveCanvasToStorage
    ・途中で失敗したら catch してユーザ通知。
  5. readAsDataURL(file)
    ・FileReader を Promise 化すると await で書けて読みやすい。
    ・成功で reader.result(Base64)が得られる。

まとめ

imageArea.js は「ユーザが画像を選ぶ即時プレビュー永続化」までをカバーしました。
 次回は、描画済み画像に対して エフェクトを適用する imageEffects.js を深掘りし、Canvas 操作の楽しさを最大化する実装テクニックを紹介します。