2026年7月2日
2026年7月2日
WordPressでDrag and Drop APIを使ったファイルアップロードUIを実装する方法
ブラウザの Drag and Drop API と WordPress の REST API(/wp/v2/media)を組み合わせると、ファイルを枠に落とすだけでメディアライブラリへアップロードできる UI を、少ないコードで作れます。肝心なのは、ドロップ操作はあくまで入力手段の一つで、ファイル選択ボタンを代わりに用意しておくこと、そして形式や容量の判定はサーバー側に任せることです。この記事では、ドロップゾーンのイベント処理、進捗表示つきのアップロード、権限と nonce の扱い、よくある失敗の見分け方までを順に説明します。
この実装が向いている場面と向かない場面
WordPress の管理画面のメディアライブラリや、ブロックエディタの画像ブロックには、すでにドラッグ&ドロップでのアップロード機能があります。管理画面の中で同じものを作り直す必要は通常ありません。自作が役に立つのは、たとえば次のような場面です。
- 投稿者権限を持つメンバーが、フロントエンドの専用ページから素材を入稿する
- 独自の管理画面(設定ページなど)に、特定の用途向けのアップロード欄を置く
一方、ログインしていない訪問者からファイルを受け付ける用途(応募フォームなど)には、この記事の方法をそのまま使わないでください。REST API の /wp/v2/media は upload_files 権限を持つユーザーだけが使える前提の入口です。匿名アップロードは攻撃の入り口になりやすく、保存場所や実行防止、ウイルス対策まで含めた設計が必要になります。その場合は、ファイル添付に対応した保守されているフォームプラグインを使うほうが安全です。
全体の構成
- PHP:アップロード権限のあるユーザーにだけドロップゾーンとスクリプトを出し、REST API の URL と nonce を渡す
- HTML:ドロップゾーン兼ファイル選択ボタン、進捗の表示欄
- JavaScript:ドラッグ系イベントの処理と、
XMLHttpRequestによる進捗つきアップロード
コードは子テーマの functions.php か mu-plugin に置きます。本番環境に入れる前に、ステージング環境で試し、バックアップを取っておいてください。
PHP:ショートコードとスクリプトの読み込み
<?php
// [dropzone_upload] ショートコードでドロップゾーンを出す
add_shortcode( 'dropzone_upload', function () {
if ( ! current_user_can( 'upload_files' ) ) {
return '<p>アップロードするにはログインが必要です。</p>';
}
wp_enqueue_script(
'my-dropzone',
get_stylesheet_directory_uri() . '/js/dropzone.js',
array(),
'1.0.0',
array( 'strategy' => 'defer', 'in_footer' => true )
);
wp_add_inline_script( 'my-dropzone', 'const MYDROP = ' . wp_json_encode( array(
'endpoint' => rest_url( 'wp/v2/media' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'maxBytes' => wp_max_upload_size(),
) ) . ';', 'before' );
return '<div class="my-dropzone">'
. '<label class="my-dropzone__area">'
. '<span>ここにファイルをドロップ、またはクリックして選択</span>'
. '<input type="file" multiple accept="image/jpeg,image/png,image/webp,application/pdf">'
. '</label>'
. '<ul class="my-dropzone__list" aria-live="polite"></ul>'
. '</div>';
} );
ショートコードの中で wp_enqueue_script() を呼ぶと、そのショートコードを置いたページでだけスクリプトが読み込まれます。wp_max_upload_size() はサーバーの upload_max_filesize と post_max_size から算出された上限値で、ブラウザ側で事前に容量を案内するために渡しています。strategy 引数は WordPress 6.3 以降の書き方です。
ドロップゾーンを <label> にして中に <input type="file"> を入れておくと、クリックやキーボード操作でもファイル選択画面が開きます。スマートフォンではデスクトップのようにファイルをドラッグできない場面が多いため、この代替手段は飾りではなく、必要な機能です。
JavaScript:ドロップの受け付けとアップロード
( function () {
const zone = document.querySelector( '.my-dropzone__area' );
const input = zone ? zone.querySelector( 'input[type="file"]' ) : null;
const list = document.querySelector( '.my-dropzone__list' );
if ( ! zone || ! input || ! list || typeof MYDROP === 'undefined' ) {
return;
}
// ゾーンの外に落としたとき、ブラウザがファイルを開いてページ遷移するのを防ぐ
[ 'dragover', 'drop' ].forEach( ( type ) => {
window.addEventListener( type, ( e ) => e.preventDefault() );
} );
let depth = 0;
const isFileDrag = ( e ) => e.dataTransfer && Array.from( e.dataTransfer.types ).includes( 'Files' );
zone.addEventListener( 'dragenter', ( e ) => {
if ( ! isFileDrag( e ) ) return;
depth++;
zone.classList.add( 'is-dragover' );
} );
zone.addEventListener( 'dragleave', () => {
depth = Math.max( 0, depth - 1 );
if ( depth === 0 ) zone.classList.remove( 'is-dragover' );
} );
zone.addEventListener( 'dragover', ( e ) => {
if ( ! isFileDrag( e ) ) return;
e.preventDefault(); // これが無いと drop イベントが発生しない
e.dataTransfer.dropEffect = 'copy';
} );
zone.addEventListener( 'drop', ( e ) => {
e.preventDefault();
depth = 0;
zone.classList.remove( 'is-dragover' );
handleFiles( e.dataTransfer.files );
} );
input.addEventListener( 'change', () => {
handleFiles( input.files );
input.value = '';
} );
async function handleFiles( fileList ) {
// 同時に送らず、1件ずつ順番に送ってサーバーの負荷を抑える
for ( const file of Array.from( fileList ) ) {
const item = document.createElement( 'li' );
item.textContent = file.name + ':待機中';
list.appendChild( item );
if ( file.size > MYDROP.maxBytes ) {
item.textContent = file.name + ':サイズが上限を超えています';
continue;
}
try {
const media = await uploadFile( file, ( percent ) => {
item.textContent = file.name + ':' + percent + '%';
} );
item.textContent = file.name + ':完了(ID ' + media.id + ')';
} catch ( err ) {
item.textContent = file.name + ':失敗(' + err.message + ')';
}
}
}
function uploadFile( file, onProgress ) {
return new Promise( ( resolve, reject ) => {
const xhr = new XMLHttpRequest();
xhr.open( 'POST', MYDROP.endpoint );
xhr.setRequestHeader( 'X-WP-Nonce', MYDROP.nonce );
xhr.upload.addEventListener( 'progress', ( e ) => {
if ( e.lengthComputable ) {
onProgress( Math.round( ( e.loaded / e.total ) * 100 ) );
}
} );
xhr.addEventListener( 'load', () => {
let data = null;
try {
data = JSON.parse( xhr.responseText );
} catch ( e ) {}
if ( xhr.status >= 200 && xhr.status < 300 && data ) {
resolve( data );
} else {
reject( new Error( ( data && data.message ) || 'HTTP ' + xhr.status ) );
}
} );
xhr.addEventListener( 'error', () => reject( new Error( '通信エラー' ) ) );
const form = new FormData();
form.append( 'file', file, file.name );
xhr.send( form );
} );
}
} )();
コードの要点と、つまずきやすいところ
dragover で preventDefault しないと drop が来ない
Drag and Drop API の一番の落とし穴です。ブラウザは既定では要素へのドロップを受け付けないため、dragover の既定動作を止めて「ここは受け付ける」と知らせる必要があります。あわせて window 全体でも dragover と drop の既定動作を止めておくと、枠を外して落としたときにブラウザが画像を開いてページから離れてしまう事故を防げます。
dragleave が子要素に入るたびに発生する
ドロップゾーンの中の <span> や <input> にカーソルが乗ると、親要素で dragleave が発生し、強調表示がちらつきます。上のコードでは dragenter と dragleave の回数を数え、0 に戻ったときだけ強調を外しています。CSS で子要素に pointer-events: none を指定する方法もありますが、<input> をクリックできなくなる場合があるので注意してください。
ファイル名を innerHTML で表示しない
ファイル名は利用者が自由に付けられる文字列で、<img src=x onerror=...> のような名前も作れます。一覧への表示は textContent を使い、HTML として解釈させないようにしています。
進捗表示に fetch() ではなく XMLHttpRequest を使う理由
fetch() には、アップロードの進み具合を受け取る仕組みが広く使える形では用意されていません(2026年10月現在)。進捗バーが要らなければ fetch() に FormData を渡すだけで足りますが、大きなファイルを扱うなら XMLHttpRequest の upload.onprogress を使うほうが利用者に親切です。
形式と容量の制限はサーバー側で決める
accept 属性や JavaScript の容量チェックは、選びやすくするための案内にすぎません。REST API は直接呼び出せるため、許可する形式の判断は WordPress 側が行います。WordPress は wp_check_filetype_and_ext() で拡張子と中身の整合を確認し、許可リストにない形式を拒否します。許可する形式を絞りたい場合は upload_mimes フィルターを使います。
<?php
// 投稿者以下の権限では、画像とPDFだけを許可する例
add_filter( 'upload_mimes', function ( $mimes ) {
if ( current_user_can( 'edit_others_posts' ) ) {
return $mimes;
}
return array(
'jpg|jpeg|jpe' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
);
} );
容量の上限は PHP の upload_max_filesize と post_max_size、Web サーバー側の設定(Nginx の client_max_body_size など)で決まります。どれか一つでも小さいと、その値で止まります。
アップロードが失敗するときの見分け方
開発者ツールのネットワークタブで media へのリクエストを開き、ステータスコードとレスポンス本文を確認すると、原因の見当がつきます。可能性の高い順に並べます。
- **401 または 403(
rest_cannot_createなど)**:ログインしていない、upload_files権限が無い、または nonce が送られていない・古いケースです。ページを長時間開いたままにすると nonce の有効期限が切れることがあるので、再読み込みして試します。 - 413(Request Entity Too Large):Web サーバーの受け付け上限に達しています。WordPress まで届いていないため、PHP の設定ではなくサーバー設定を確認します。逆に PHP の
post_max_sizeを超えた場合は、本文が空として扱われ、「データが無い」という趣旨の 400 エラーになることがあります。 - 400 や 500 で、本文にファイル形式のエラー:許可されていない形式か、拡張子と中身が一致しないファイルです。
upload_mimesの設定と、ファイル自体を確認します。 - 500 で本文が空、または HTML のエラーページ:PHP のメモリ不足や、画像サイズ生成時のエラーが考えられます。
WP_DEBUG_LOGを有効にしてwp-content/debug.logを確認します。
まとめ
- ドロップゾーンには
<label>と<input type="file">を組み合わせ、ドラッグできない環境やキーボード操作でも使えるようにします。 dragoverでpreventDefault()しないとdropは発生しません。window全体でも既定動作を止めると誤遷移を防げます。- アップロード先は
/wp/v2/media、認証はwp_create_nonce( 'wp_rest' )をX-WP-Nonceヘッダーで送り、upload_files権限のあるユーザーに限定します。 - 形式と容量の判断はサーバー側(
upload_mimes、PHP と Web サーバーの上限)で行い、ブラウザ側の検証は案内として扱います。 - 失敗したときは、ネットワークタブのステータスコードから原因を絞り込みます。