概要

ファイル送信を含むフォームの開発案件では次のような要望が多い。

(1) ドラッグ&ドロップでファイルを選択させたい。
(2) ブラウザによって見た目が変わらない「ファイルを選択」ボタンも併設したい。
(3) 入力画面>確認画面>完了画面という遷移にしたい。
(4) ファイルを選択したら確認画面に繊維せずともプレビュー表示させたい。
(5) 確認画面に遷移する前にファイルタイプやファイルサイズで簡易バリデートしたい。
(6) もちろん確認画面でもプレビューさせたい。
(7) 確認画面から入力画面に戻ったときも選択したファイルは維持させたい。
(8) 完了画面に遷移するまでサーバーにはファイルは保存させたくない。

従来の方法では上記要望を叶える為には色々と面倒だったが、HTML5のFileAPIが登場で随分と楽になったのでメモ。

ファイル例

form.php (入力画面)

<!DOCTYPE html>
<html>
<head>
<title>入力画面</title>
<meta charset="utf-8">
<link rel="stylesheet" href="form.css">
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="form.js"></script>
</head>
<body>
  <h3>ファイル送信フォーム</h3>
  <form action="conf.php" method="post">
    <!-- ドロップエリア -->
    <div id="DropArea">ファイルをドロップしてください。</div>
    <!-- 送信ボタン -->
    <label class="btn">
      <input type="file" name="file_form">
      ファイル選択
    </label>
    <!-- プレビュー -->
    <div id="Preview"><?php echo !empty($_POST['file_data']) ? '<img src="' . $_POST['file_data'] . '">' : ''; ?></div>
    <!-- ファイル情報 -->
    <div id="Information">
      <?php
        if (!empty($_POST['file_size'])) {
          echo 'ファイルサイズ: ' . (round($_POST['file_size'] / 10000) / 100) . 'MB';
        }
      ?>
    </div>
    <!-- メッセージ -->
    <div id="Message"></div>
    <!-- hidden値 -->
    <input type="hidden" name="file_data" value="<?php echo !empty($_POST['file_data']) ? $_POST['file_data'] : ''; ?>">
    <input type="hidden" name="file_name" value="<?php echo !empty($_POST['file_name']) ? $_POST['file_name'] : ''; ?>">
    <input type="hidden" name="file_size" value="<?php echo !empty($_POST['file_size']) ? $_POST['file_size'] : ''; ?>">
    <input type="hidden" name="file_type" value="<?php echo !empty($_POST['file_type']) ? $_POST['file_type'] : ''; ?>">
    <!-- 送信ボタン -->
    <input type="submit" value="送信">
  </form>
</body>
</html>

conf.php(確認画面)

<!DOCTYPE html>
<html>
<head>
<title>確認画面</title>
<meta charset="utf-8">
<link rel="stylesheet" href="form.css">
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>ファイル送信フォーム</h3>
  <form action="comp.php" method="post">
    <!-- プレビュー -->
    <div id="Preview"><?php echo !empty($_POST['file_data']) ? '<img src="' . $_POST['file_data'] . '">' : ''; ?></div>
    <!-- ファイル情報 -->
    <div id="Information">
      <?php
        if (!empty($_POST['file_size'])) {
          echo 'ファイルサイズ: ' . (round($_POST['file_size'] / 10000) / 100) . 'MB';
        }
      ?>
    </div>
    <!-- hidden値 -->
    <input type="hidden" name="file_data" value="<?php echo !empty($_POST['file_data']) ? $_POST['file_data'] : ''; ?>">
    <input type="hidden" name="file_name" value="<?php echo !empty($_POST['file_name']) ? $_POST['file_name'] : ''; ?>">
    <input type="hidden" name="file_size" value="<?php echo !empty($_POST['file_size']) ? $_POST['file_size'] : ''; ?>">
    <input type="hidden" name="file_type" value="<?php echo !empty($_POST['file_type']) ? $_POST['file_type'] : ''; ?>">
    <!-- 送信ボタン -->
    <input type="submit" value="送信">
    <input type="button" value="戻る" onclick="$('form').attr('action', 'form.php').submit();">
  </form>
</body>
</html>

comp.php(完了画面)

<?php
  // ファイル処理
  if (
    !empty($_POST['file_data'])
    && !empty($_POST['file_name'])
    && !empty($_POST['file_type'])
    && !empty($_POST['file_size'])
    && ($ascii = explode(',', $_POST['file_data']))
    && !empty($ascii[1])
    && ($bin = base64_decode($ascii[1]))
  ) {
    // ファイル保存
    $path = 'files/' . $_POST['file_name'];
    file_put_contents($path, $bin);
  }
?>
<!DOCTYPE html>
<html>
<head>
<title>完了画面</title>
<meta charset="utf-8">
<link rel="stylesheet" href="form.css">
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
</head>
<body>
  <h3>ファイル送信フォーム</h3>
  <p>ありがとうございました。</p>
</body>
</html>

form.css

/* ドロップエリア*/
#DropArea {
  width: 300px;
  height: 100px;
  border: 5px dashed #AFA;
}
/* ドロップエリア hover時 */
#DropArea.hover {
  border: 5px dashed #AAF;
}
/* ファイル選択ボタン */
input[type=file] {
  display: none;
}
.btn {
  background-color: #EEE;
  border: 1px solid #CCC;
}
/* プレビュー */
#Preview {
    width: 300px;
}
#Preview img{
    width: 100%;
}

form.js

jQuery(function($){
  // イベント登録
  // bodyドラッグ開始時
  $(document).on('dragover', 'body', function(e){
    // イベントキャンセル
    e.preventDefault();
  });
  // bodyドラッグ終了時
  $(document).on('dragleave', 'body', function(e){
    // イベントキャンセル
    e.preventDefault();
  });
  // bodyドロップ時
  $(document).on('drop', 'body', function(e){
    // イベントキャンセル
    e.preventDefault();
  });
  // ドロップエリアドラッグ開始時
  $(document).on('dragover', '#DropArea', function(e){
    // イベントキャンセル
    e.preventDefault();
    // class付加
    $(this).addClass('hover');
  });
  // ドロップエリアドラッグ終了時
  $(document).on('dragleave', '#DropArea', function(e){
    // イベントキャンセル
    e.preventDefault();
    // class削除
    $(this).removeClass('hover');
  });
  // ドロップエリアドロップ時
  $(document).on('drop', '#DropArea', function(e){
    // イベントキャンセル
    e.preventDefault();
    // class削除
    $('#DropArea').removeClass('hover');
    // ファイル取得
    var file = e.originalEvent.dataTransfer.files[0];
    // ファイルをフォームにセット
    setFile(file);
  });
  // ファイル選択ボタンから選択時
  $(document).on('change', 'input[name="file_form"]', function(e){
    // ファイル取得
    var file = e.target.files[0];
    // ファイルをフォームにセット
    setFile(file);
  });
  /**
   * ファイルをフォームにセット
   */
  function setFile(file) {
    // 初期実行
    $('input[name="file_data"]').val('');
    $('input[name="file_name"]').val('');
    $('input[name="file_size"]').val('');
    $('input[name="file_type"]').val('');
    $('#Information').html('');
    $('#Preview').html('');
    $('#Message').html('');
    // ファイル取得失敗時
    if (typeof(file) !== 'object') {
      return;
    }
    // 情報表示
    var sizeStr = Math.round(file.size / 10000) / 100 + 'MB';
    $('#Information').html('ファイルサイズ: ' + sizeStr);
    // 簡易バリデート
    var errMsg = '';
    if (file.size > 2000000) {
      errMsg = 'ファイルサイズは2MBまでです。';
    } else if (!file.type.match('image.*')) {
      errMsg = '適切な画像ファイルではありません。';
    }
    if (errMsg) {
      $('#Message').html(errMsg);
      return;
    }
    // ファイルリーダー
    var fileReader = new FileReader();
    // イベント - 開始時
    fileReader.onloadstart = function(e) {
      // プログレスバー作成
      var html = '
'; $('#Preview').html(html); }; // イベント - ロード時 fileReader.onprogress = function(e) { if (e.lengthComputable) { // プログレスバーの中身作成 var percentLoaded = Math.round((e.loaded / e.total) * 100); if (percentLoaded < 100) { $('.progress .bar').css({width: percentLoaded + '%'}); } } }; // イベント - 終了時 fileReader.onloadend = function(e) { // プログレスバー削除 $('.progress').remove(); }; // イベント - 完了時 fileReader.onload = function(e) { // プレビュー $('#Preview').html(''); // フォーム更新 $('input[name="file_data"]').val(e.target.result); $('input[name="file_name"]').val(file.name); $('input[name="file_size"]').val(file.size); $('input[name="file_type"]').val(file.type); }; // 読み込み開始 fileReader.readAsDataURL(file); } });