画像をアップロードするドラッグ&ドロップ枠のコードと実行結果

ほわもん
質問: 画像をドラッグ&ドロップできる枠
Webアプリで画像をドロップするとプレビュー画像が表示できて、アップロードできる枠がありますが、どうやって実装するのですか?

実装方針

Webアプリで画像をドロップするとプレビュー画像が表示されるアップロード枠を実装する場合、 枠自体はdiv等のタグで記述します。 アップロードはダイアログ等を表示することも考慮して、inputタグにするのが一般的ですので、input type="file" を枠内に記述します。
画像がドロップされると、inputタグに画像ファイルを設定し、imgタグをドロップ枠内に作成して画像を表示します。

コード

以下のHTMLファイル,CSSファイルを作成します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="SimpleImageDragDrop.css" />

  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function () {

      const dropArea = document.getElementById('ImageDropArea');
      const preview = document.getElementById('gallery');
      const fileElem = document.getElementById('fileElem');
      const dropText = document.getElementById('dropText');

      // preventDefaults:全イベントでデフォルトを止める
      function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
      }

      // 枠クリックでファイル選択ダイアログを開く
      dropArea.addEventListener('click', () => {
        fileElem.click();
      });

      // dragover をキャンセルしないと drop が発火しない
      // 対象要素に一括でイベント登録
      ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
      });

      dropArea.addEventListener('dragover', () => dropArea.classList.add('highlight'));
      dropArea.addEventListener('dragleave', () => dropArea.classList.remove('highlight'));
      dropArea.addEventListener('drop', () => dropArea.classList.remove('highlight'));

      // これを追加しないと、ダイアログから選択しても showPreview が呼ばれない
      fileElem.addEventListener('change', e => {
        if (e.target.files.length) {
          showPreview(e.target.files[0]);
        }
      });

      // ドロップ時
      dropArea.addEventListener('drop', e => {
        const dt = e.dataTransfer;
        if (dt.files.length) {
          showPreview(dt.files[0]);
          //
          const trans = new DataTransfer();
          trans.items.add(dt.files[0]);
          // これで input.files が上書きできる
          fileElem.files = trans.files;
        }
      });

      // プレビュー表示(1枚のみ)
      function showPreview(file) {
        // instruction を非表示
        dropText.style.display = 'none';

        if (!file.type.startsWith('image/')) return;

        const reader = new FileReader();
        reader.onload = () => {
          // 既存プレビューをクリア
          preview.innerHTML = '';
          // 画像要素を生成
          const img = document.createElement('img');
          img.src = reader.result;
          preview.appendChild(img);
        };
        reader.readAsDataURL(file);
      }

    });

  </script>

</head>
<body>
  <h1>画像のドラッグ・アンド・ドロップ枠のデモ</h1>

  <form id="uploadForm" action="/upload" method="POST" enctype="multipart/form-data">

    <div id="ImageDropArea" class="ImageDropAreaStyle">
      <p id="dropText">ここに画像ファイルをドロップしてください</p>
      <input type="file" id="fileElem" accept="image/*" hidden />
      <div id="gallery"></div>
    </div>

    <button type="submit">送信</button>
  </form>

</body>
</html>
.ImageDropAreaStyle {
  width: 480px;
  border: 2px dashed #303030;
  margin:1rem 0 1rem 0;
}

  .ImageDropAreaStyle.highlight {
    border: 2px dashed #00e1da;
  }

  .ImageDropAreaStyle img {
    width:100%;
  }

解説

preventDefault() stopPropagation() メソッドを呼び出してデフォルトのイベントを止めます。 この処理がないと、dropイベントが発火しません。また、ドロップでブラウザの新しいウィンドウ/タブが開いて画像が表示されてしまいます。

  // preventDefaults:全イベントでデフォルトを止める
  function preventDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
  }

  // dragover をキャンセルしないと drop が発火しない
  // 対象要素に一括でイベント登録
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
    dropArea.addEventListener(eventName, preventDefaults, false);
  });


こちらのコードで画像をドラッグして枠に重ねた際の見栄えを変更します。

      dropArea.addEventListener('dragover', () => dropArea.classList.add('highlight'));
      dropArea.addEventListener('dragleave', () => dropArea.classList.remove('highlight'));
      dropArea.addEventListener('drop', () => dropArea.classList.remove('highlight'));


枠をクリックするとファイル選択ダイアログを開きます。

  // 枠クリックでファイル選択ダイアログを開く
  dropArea.addEventListener('click', () => {
    fileElem.click();
  });


画像が枠にドロップされると以下のコードを実行します。プレビュー画像の表示とinputタグへの設定をします。

  // ドロップ時
  dropArea.addEventListener('drop', e => {
    const dt = e.dataTransfer;
    if (dt.files.length) {
      showPreview(dt.files[0]);
      //
      const trans = new DataTransfer();
      trans.items.add(e.target.files[0]);
      // これで input.files が上書きできる
      fileElem.files = trans.files;
    }
  });


ダイアログから選択した場合は、プレビューの表示をします。

  fileElem.addEventListener('change', e => {
    if (e.target.files.length) {
      showPreview(e.target.files[0]);
    }
  });
  // プレビュー表示(1枚のみ)
  function showPreview(file) {
    // instruction を非表示
    dropText.style.display = 'none';

    if (!file.type.startsWith('image/')) return;

    const reader = new FileReader();
    reader.onload = () => {
      // 既存プレビューをクリア
      preview.innerHTML = '';
      // 画像要素を生成
      const img = document.createElement('img');
      img.src = reader.result;
      preview.appendChild(img);
    };
    reader.readAsDataURL(file);
  }

実行結果

上記のページを表示します。下図の画面が表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像1

画像ファイルをドラッグします。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像2

画像ファイルを枠に重ねると枠の色が変わります。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像3

画像をドロップすると、画像のサムネイルが枠内に表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像4

なお、枠をクリックすると、ファイル選択ダイアログが表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像5

ダイアログで選択したファイルのサムネイルが枠に表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像6

応用例: RazorPages でのアップロード

上記のドラッグ&ドロップ枠を利用して、RazorPagesでファイルをアップロードするコードを紹介します。

コード

\Pages\Upload.cshtml
@page
@model FileUploadDnD.Pages.UploadModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
}
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="AppStyle.css" />

  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function () {

      const dropArea = document.getElementById('ImageDropArea');
      const preview = document.getElementById('gallery');
      const fileElem = document.getElementById('fileElem');
      const dropText = document.getElementById('dropText');

    @if (ViewData["Thumbnail"] != null) {
      @: showUploadPreview();
    }

      // preventDefaults:全イベントでデフォルトを止める
      function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
      }

      // 枠クリックでファイル選択ダイアログを開く
      dropArea.addEventListener('click', () => {
        fileElem.click();
      });

      // dragover をキャンセルしないと drop が発火しない
      // 対象要素に一括でイベント登録
      ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
      });

      dropArea.addEventListener('dragover', () => dropArea.classList.add('highlight'));
      dropArea.addEventListener('dragleave', () => dropArea.classList.remove('highlight'));
      dropArea.addEventListener('drop', () => dropArea.classList.remove('highlight'));

      // これを追加しないと、ダイアログから選択しても showPreview が呼ばれない
      fileElem.addEventListener('change', e => {
        if (e.target.files.length) {
          showPreview(e.target.files[0]);
        }
      });

      // ドロップ時
      dropArea.addEventListener('drop', e => {
        const dt = e.dataTransfer;
        if (dt.files.length) {
          showPreview(dt.files[0]);
          //
          const trans = new DataTransfer();
          trans.items.add(dt.files[0]);
          // これで input.files が上書きできる
          fileElem.files = trans.files;
        }
      });

      // プレビュー表示(1枚のみ)
      function showPreview(file) {
        // instruction を非表示
        dropText.style.display = 'none';

        if (!file.type.startsWith('image/')) return;

        const reader = new FileReader();
        reader.onload = () => {
          // 既存プレビューをクリア
          preview.innerHTML = '';
          // 画像要素を生成
          const img = document.createElement('img');
          img.src = reader.result;
          preview.appendChild(img);
        };
        reader.readAsDataURL(file);
      }


     function showUploadPreview() {
       // instruction を非表示
       dropText.style.display = 'none';

       //アップロードされた画像データを表示
       const img = document.createElement('img');
       img.src = '@Html.Raw(ViewData["Thumbnail"])';
       preview.appendChild(img);
    }

    });

  </script>

</head>
<body>
  <h1>画像のドラッグ・アンド・ドロップによるアップロード</h1>

  <form id="uploadForm" action="" method="POST" enctype="multipart/form-data">

    <div id="ImageDropArea" class="ImageDropAreaStyle">
      <p id="dropText">ここに画像ファイルをドロップしてください</p>
      <input type="file" id="fileElem" accept="image/*" asp-for="FileData" hidden />
      <div id="gallery"></div>
    </div>

    <button type="submit">送信</button>
  </form>
  <hr />
  <div>ファイルサイズ:@Model.FileSize</div>

</body>
</html>

\Pages\Upload.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using static System.Net.Mime.MediaTypeNames;

namespace FileUploadDnD.Pages
{
  public class UploadModel : PageModel
  {
    [BindProperty]
    public IFormFile FileData { get; set; }

    public int FileSize { get; set; }

    public void OnGet()
    {
    }

    public IActionResult OnPost()
    {
      if (FileData == null || FileData.Length == 0) {
        ModelState.AddModelError("FileData", "ファイルが選択されていません。");
        return Page();
      }
      else {
        try {
          // サムネイル用のBase64エンコード
          using var memoryStream = new MemoryStream();
          FileData.CopyTo(memoryStream); // IFormFile の内容をメモリストリームにコピー
          byte[] fileBytes = memoryStream.ToArray(); // バイト配列に変換
          var base64Image = Convert.ToBase64String(memoryStream.ToArray());
          ViewData["Thumbnail"] = $"data:{FileData.ContentType};base64,{base64Image}";
        }
        catch (Exception ex) {
          ModelState.AddModelError(string.Empty, $"エラーが発生しました: {ex.Message}");
          return Page();
        }

        FileSize = (int)FileData.Length;
        return Page();
      }
    }
  }
}

AppStyle.cs
.ImageDropAreaStyle {
  width: 480px;
  height: 320px;
  border: 2px dashed #303030;
  margin: 1rem 0 1rem 0;
  padding: 0.5rem 0.5rem 0.5rem 0.5rem;
  /*padding: 0 0 0 0;*/
}

  .ImageDropAreaStyle.highlight {
    border: 2px dashed #00e1da;
  }

#gallery {
  width: 100%;
  height: 100%;
}
.ImageDropAreaStyle img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

Program.cs
namespace FileUploadDnD
{
  public class Program
  {
    public static void Main(string[] args)
    {
      var builder = WebApplication.CreateBuilder(args);
      builder.Services.AddRazorPages();
      var app = builder.Build();

      app.UseStaticFiles();
      app.UseRouting();
      app.MapRazorPages();
      
      app.Run();
    }
  }
}

解説

JavaScript部分は先に紹介したコードと同様です。

アップロード後に画像が枠内に表示されるよう、アップロードされた画像のデータは ViewData["Thumbnail"] に代入します。
代入する際の書式は以下になります。

data:image/png;base64,(base64形式でエンコードされた画像データ)
data:image/jpeg;base64,(base64形式でエンコードされた画像データ)
    public IActionResult OnPost()
    {
      if (FileData == null || FileData.Length == 0) {
        ModelState.AddModelError("FileData", "ファイルが選択されていません。");
        return Page();
      }
      else {
        try {
          // サムネイル用のBase64エンコード
          using var memoryStream = new MemoryStream();
          FileData.CopyTo(memoryStream); // IFormFile の内容をメモリストリームにコピー
          byte[] fileBytes = memoryStream.ToArray(); // バイト配列に変換
          var base64Image = Convert.ToBase64String(memoryStream.ToArray());
          ViewData["Thumbnail"] = $"data:{FileData.ContentType};base64,{base64Image}";
        }
        catch (Exception ex) {
          ModelState.AddModelError(string.Empty, $"エラーが発生しました: {ex.Message}");
          return Page();
        }

        FileSize = (int)FileData.Length;
        return Page();
      }
    }


ViewData["Thumbnail"] に値が設定されている場合は、showUploadPreview()関数を呼び出すコードをページ表示時に作成し、 ページ表示時に、showUploadPreview関数を呼び出し、枠内にアップロードした画像を表示します。

    @if (ViewData["Thumbnail"] != null) {
      @: showUploadPreview();
    }
画像データの受け渡しが気になる場合
画像サイズが大きい場合、画像データが ViewData["Thumbnail"] に代入されるため、アップロード後のレスポンスサイズが 画像データのサイズぶん大きくなります。レスポンスの増加を避けたい場合は、POSTによる画像の送信ではなく、AJAX化してWeb APIを非同期で呼び出すことで、 ページ切り替えが発生しないため、画像データの再取得が不要になり、レスポンスサイズが大きくならずに実装できます。

実行結果

プロジェクトを実行し、(アプリケーションルートURL)/Upload にアクセスします。下図のページが表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像7

画像を枠内にドラッグしてドロップします。ドロップすると枠内に画像が表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像8 画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像9

[送信]ボタンをクリックします。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像10

画像がアップロードされ、画像のファイルサイズがページ下部に表示されます。
画像をアップロードするドラッグ&ドロップ枠のコードと実行結果:画像11

AuthorPortraitAlt
著者
iPentecのプログラマー、最近はAIの積極的な活用にも取り組み中。
とっても恥ずかしがり。
作成日: 2025-04-12