AjaxとファイルのDrag & Drop処理


ここでは,通常の <input type="file"> だけを使う提出フォームを一歩進め,ファイルをドラッグ&ドロップで指定し,ページ遷移なしにAjaxでアップロードする例を作成します。

本節は第5章の完成版に近い段階で使う例なので,データベース接続とログイン確認には common/common.php を使います。前半で作成した dbconnect.php だけの段階に戻す場合は,ログイン確認部分を省略し,PDO接続だけを読み込む形に読み替えてください。

JavaScript側では,選択されたファイル名,拡張子,サイズを確認できます。ただし,これは利用者の操作性を高めるための補助にすぎません。ファイルサイズ,拡張子,MIMEタイプ,保存ファイル名の決定は,必ずPHP側でも検査します。

Drag & Dropでファイルを受け取る考え方

ブラウザ上のドロップ領域では,主に次のイベントを扱います。

ここにファイルをドラッグ&ドロップする,という見た目の領域を作ります。

FormDataとAjaxによる送信

ファイルは FormData オブジェクトに追加し,fetch() を使ってPHPスクリプトへ非同期送信します。従来は XMLHttpRequest を直接使う例が多くありましたが,現在の教材では同じAjax処理をより簡潔に書ける fetch() を使います。

task_dragdrop.php

<?php
// task_dragdrop.php: Drag & Drop + Ajaxによる課題提出画面
session_start();
require_once __DIR__ . '/common/common.php';
$member = login_check();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>課題提出(Drag & Drop版)</title>
<style>
.drop-zone {
    border: 3px dashed #0d6efd;
    border-radius: 12px;
    padding: 2rem;
    text-align: center;
    background: #f8f9fa;
    cursor: pointer;
}
.drop-zone.dragover {
    background: #e7f1ff;
    border-color: #084298;
}
#message { margin-top: 1rem; }
</style>
</head>
<body>
<h1>課題提出(Drag & Drop版)</h1>

<form id="upload_form">
    <p>課題名:<input type="text" name="name" id="name" required></p>
    <p>コメント:<br>
        <textarea name="word" id="word" rows="5" cols="50"></textarea>
    </p>

    <div id="drop_zone" class="drop-zone">
        ここに pdf, zip, txt ファイルをドラッグ&ドロップしてください。<br>
        またはクリックしてファイルを選択してください。<br>
        <small>最大サイズ: 5 MB</small>
    </div>

    <input type="file" id="task_file" name="task_file" accept=".pdf,.zip,.txt" hidden>
    <p id="file_info"></p>
    <p><button type="submit">ファイル送信</button></p>
</form>

<div id="message"></div>

<script>
const dropZone = document.getElementById('drop_zone');
const fileInput = document.getElementById('task_file');
const fileInfo = document.getElementById('file_info');
const message = document.getElementById('message');
const form = document.getElementById('upload_form');
let selectedFile = null;

function showFile(file) {
    const allowedExt = ['pdf', 'zip', 'txt'];
    const maxSize = 5 * 1024 * 1024;
    const ext = file.name.split('.').pop().toLowerCase();

    if (!allowedExt.includes(ext)) {
        selectedFile = null;
        fileInfo.textContent = '';
        alert('アップロードできるファイルは pdf, zip, txt のみです。');
        return;
    }

    if (file.size > maxSize) {
        selectedFile = null;
        fileInfo.textContent = '';
        alert('ファイルサイズは5MB以下にしてください。');
        return;
    }

    selectedFile = file;
    fileInfo.textContent = `${file.name} (${file.size} bytes)`;
}

dropZone.addEventListener('click', () => fileInput.click());

fileInput.addEventListener('change', () => {
    if (fileInput.files.length > 0) {
        showFile(fileInput.files[0]);
    }
});

['dragenter', 'dragover'].forEach(eventName => {
    dropZone.addEventListener(eventName, event => {
        event.preventDefault();
        event.stopPropagation();
        dropZone.classList.add('dragover');
    });
});

['dragleave', 'drop'].forEach(eventName => {
    dropZone.addEventListener(eventName, event => {
        event.preventDefault();
        event.stopPropagation();
        dropZone.classList.remove('dragover');
    });
});

dropZone.addEventListener('drop', event => {
    const files = event.dataTransfer.files;
    if (files.length > 0) {
        showFile(files[0]);
    }
});

form.addEventListener('submit', async event => {
    event.preventDefault();

    if (!selectedFile) {
        alert('提出するファイルを選択してください。');
        return;
    }

    const formData = new FormData();
    formData.append('name', document.getElementById('name').value);
    formData.append('word', document.getElementById('word').value);
    formData.append('task_file', selectedFile);

    message.textContent = 'アップロード中です...';

    try {
        const response = await fetch('task_ajax_upload.php', {
            method: 'POST',
            body: formData
        });

        const result = await response.json();

        if (result.ok) {
            message.textContent = result.message;
            form.reset();
            selectedFile = null;
            fileInfo.textContent = '';
        } else {
            message.textContent = result.message;
        }
    } catch (error) {
        message.textContent = '通信エラーが発生しました。';
    }
});
</script>
</body>
</html>

このスクリプトでは,見た目のドロップ領域 drop_zone と,実際にファイルを保持する非表示の input type="file" を組み合わせています。クリックした場合は通常のファイル選択,ドラッグ&ドロップした場合は event.dataTransfer.files から受け取ったファイルを selectedFile に保存します。

PHP側のアップロード処理

Ajaxで送られてきたファイルは,通常のフォーム送信と同じく $_FILES に入ります。違いは,処理結果をHTMLページとして返すのではなく,JSONとして返す点です。JavaScript側はそのJSONを受け取り,画面のメッセージ欄を書き換えます。

task_ajax_upload.php

<?php
// task_ajax_upload.php: Drag & Drop + Ajax用アップロード処理
session_start();
require_once __DIR__ . '/common/common.php';
$member = login_check();

header('Content-Type: application/json; charset=UTF-8');

function json_response(bool $ok, string $message): void
{
    echo json_encode(['ok' => $ok, 'message' => $message], JSON_UNESCAPED_UNICODE);
    exit();
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    json_response(false, '不正なリクエストです。');
}

if (empty($_POST['name'])) {
    json_response(false, '課題名を入力してください。');
}

if (!isset($_FILES['task_file']) || $_FILES['task_file']['error'] !== UPLOAD_ERR_OK) {
    json_response(false, 'ファイルが正しくアップロードされませんでした。');
}

$file = $_FILES['task_file'];
$max_size = 5 * 1024 * 1024;
$allowed_ext = ['pdf', 'zip', 'txt'];
$allowed_mime = [
    'application/pdf',
    'application/zip',
    'application/x-zip-compressed',
    'text/plain',
];

if ($file['size'] > $max_size) {
    json_response(false, 'ファイルサイズは5MB以下にしてください。');
}

$original = basename($file['name']);
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));

if (!in_array($ext, $allowed_ext, true)) {
    json_response(false, 'アップロードできる拡張子は pdf, zip, txt のみです。');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);

if (!in_array($mime, $allowed_mime, true)) {
    json_response(false, '許可されていないファイル形式です。');
}

$upload_dir = __DIR__ . '/task_folder';
if (!is_dir($upload_dir)) {
    mkdir($upload_dir, 0755, true);
}

$save_name = date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$save_path = $upload_dir . '/' . $save_name;

if (!move_uploaded_file($file['tmp_name'], $save_path)) {
    json_response(false, 'ファイルの保存に失敗しました。');
}

$sql = 'INSERT INTO task (member, name, file, change_name, word, modified)
        VALUES (:member, :name, :file, :change_name, :word, NOW())';
$stmt = $dbh->prepare($sql);
$stmt->bindValue(':member', (int)$member['id'], PDO::PARAM_INT);
$stmt->bindValue(':name', $_POST['name'], PDO::PARAM_STR);
$stmt->bindValue(':file', $original, PDO::PARAM_STR);
$stmt->bindValue(':change_name', $save_name, PDO::PARAM_STR);
$stmt->bindValue(':word', $_POST['word'] ?? '', PDO::PARAM_STR);
$stmt->execute();

json_response(true, 'アップロードが完了しました。');

PHP側では,次の確認を行っています。

ソース表示ページ

PHPファイルへの直接リンクは置かず,ソースを表示するだけのページを用意しています。

既存システムへの組み込み

この例を既存の課題提出画面に組み込む場合は,task.php のフォーム部分を task_dragdrop.php のフォームとJavaScriptに置き換え,保存処理を task_ajax_upload.php に分離します。提出一覧や削除処理は,保存後に同じ task テーブルを参照するため,基本的にはそのまま利用できます。


Copyright (c) 2014-2026 T.Kouya Laboratory @ Otemon Gakuin University. All rights reserved.