タイトル: Drag and Drop API
SEOタイトル: HTML5 Drag and Drop API 完全ガイド(dragstart/drop/dataTransfer の使い方)
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li><strong>HTML5 Drag and Drop API</strong> はブラウザネイティブのドラッグ&ドロップ機構。要素やファイルをマウス/タッチで運べる</li><li>ドラッグ元: <code>draggable="true"</code> 属性 + <code>dragstart</code> で <code>dataTransfer.setData()</code></li><li>ドロップ先: <code>dragover</code> で <code>preventDefault()</code> 必須 → これがないと <code>drop</code> が発火しない</li><li>ファイル D&D は <code>event.dataTransfer.files</code> で取得(<code>FileList</code>)</li><li>モダンライブラリの定番: <strong>SortableJS</strong>(リスト並べ替え)/ <strong>dnd-kit</strong>(React、アクセシブル)/ <strong>react-dnd</strong></li></ul></td></tr></table>
<h2>Drag and Drop API とは</h2>
<p><strong>HTML5 Drag and Drop API</strong>(DnD API)は、ブラウザがネイティブでサポートするドラッグ&ドロップ機構です。要素を別の場所にマウス/タッチで運んだり、デスクトップからファイルをブラウザに投げ込んだりできます。jQuery UI や独自実装に頼らずに、純粋な DOM イベントだけで実現できるのが利点です。</p>
<h2>基本フロー</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>段階</th><th>発火イベント</th><th>対象</th><th>主な処理</th></tr></thead>
<tbody>
<tr><td>掴む</td><td><code>dragstart</code></td><td>ドラッグ元</td><td><code>dataTransfer.setData()</code> でデータをセット</td></tr>
<tr><td>運ぶ</td><td><code>drag</code></td><td>ドラッグ元</td><td>連続発火。UI 演出に使う</td></tr>
<tr><td>上を通過</td><td><code>dragenter</code> / <code>dragover</code></td><td>ドロップ先</td><td><strong><code>preventDefault()</code> 必須</strong></td></tr>
<tr><td>離脱</td><td><code>dragleave</code></td><td>ドロップ先</td><td>ホバー演出を解除</td></tr>
<tr><td>離す</td><td><code>drop</code></td><td>ドロップ先</td><td><code>dataTransfer.getData()</code> で受け取り</td></tr>
<tr><td>終了</td><td><code>dragend</code></td><td>ドラッグ元</td><td>クリーンアップ</td></tr>
</tbody>
</table>
<h2>最小実装例</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><div id="src" draggable="true" style="padding:1em;background:#cef">Drag me</div>
<div id="dst" style="padding:2em;border:2px dashed #999;margin-top:1em">Drop here</div>
<script>
const src = document.getElementById('src');
const dst = document.getElementById('dst');
src.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', 'hello');
e.dataTransfer.effectAllowed = 'move';
});
dst.addEventListener('dragover', (e) => {
e.preventDefault(); // ← これが無いと drop が発火しない
e.dataTransfer.dropEffect = 'move';
});
dst.addEventListener('drop', (e) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
dst.textContent = 'Dropped: ' + data;
});
</script></code></pre>
<h2>dataTransfer オブジェクト</h2>
<p>ドラッグ元とドロップ先の間でデータを受け渡すのが <code>event.dataTransfer</code> です。</p>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>プロパティ / メソッド</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>setData(type, value)</code></td><td>データを格納。<code>type</code> は MIME(<code>text/plain</code>, <code>text/html</code>, <code>application/json</code> 等)</td></tr>
<tr><td><code>getData(type)</code></td><td>データ取得。drop ハンドラ内でのみ</td></tr>
<tr><td><code>files</code></td><td>OS から D&D されたファイル(<code>FileList</code>)</td></tr>
<tr><td><code>effectAllowed</code></td><td>許可される動作 <code>copy</code> / <code>move</code> / <code>link</code> / <code>all</code></td></tr>
<tr><td><code>dropEffect</code></td><td>カーソル表示(<code>copy</code>/<code>move</code>/<code>link</code>/<code>none</code>)</td></tr>
<tr><td><code>setDragImage(el, x, y)</code></td><td>ドラッグ中のサムネ画像を差し替え</td></tr>
</tbody>
</table>
<h2>ファイルの D&D(最頻出ユースケース)</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><div id="dropzone" style="padding:3em;border:2px dashed #888;text-align:center">
Drop files here
</div>
<script>
const zone = document.getElementById('dropzone');
['dragenter', 'dragover'].forEach(t => {
zone.addEventListener(t, (e) => {
e.preventDefault();
zone.style.background = '#eef';
});
});
['dragleave', 'drop'].forEach(t => {
zone.addEventListener(t, (e) => {
e.preventDefault();
zone.style.background = '';
});
});
zone.addEventListener('drop', async (e) => {
const files = e.dataTransfer.files; // FileList
for (const f of files) {
console.log(f.name, f.size, f.type);
// 例: 画像をプレビュー
if (f.type.startsWith('image/')) {
const url = URL.createObjectURL(f);
const img = new Image();
img.src = url;
zone.appendChild(img);
}
// 例: サーバへアップロード
const fd = new FormData();
fd.append('file', f);
await fetch('/upload', { method: 'POST', body: fd });
}
});
</script></code></pre>
<h2>preventDefault を忘れる罠</h2>
<p>仕様上、ブラウザはデフォルトでは「ドロップを禁止する」動作になっています。<strong><code>dragover</code> で <code>preventDefault()</code> を呼ばないと <code>drop</code> イベントが発火しません</strong>。これが Drag and Drop API で最も多いハマりどころです。</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// ❌ NG: drop が発火しない
dst.addEventListener('drop', handler);
// ✅ OK
dst.addEventListener('dragover', (e) => e.preventDefault());
dst.addEventListener('drop', handler);</code></pre>
<h2>並べ替え(ソート)UI の実装</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><ul id="list">
<li draggable="true">Apple</li>
<li draggable="true">Banana</li>
<li draggable="true">Cherry</li>
</ul>
<script>
let dragEl = null;
document.querySelectorAll('#list li').forEach(li => {
li.addEventListener('dragstart', () => { dragEl = li; li.style.opacity = .4; });
li.addEventListener('dragend', () => { dragEl = null; li.style.opacity = 1; });
li.addEventListener('dragover', (e) => {
e.preventDefault();
const rect = li.getBoundingClientRect();
const after = (e.clientY - rect.top) > rect.height / 2;
li.parentNode.insertBefore(dragEl, after ? li.nextSibling : li);
});
});
</script></code></pre>
<h2>フォールバック / 制約</h2>
<ul>
<li><strong>モバイル(iOS Safari など)はネイティブ DnD 非対応</strong>に近い。Pointer Events や touch イベントで自前実装が必要</li>
<li>キーボード操作・スクリーンリーダー対応が弱い → <strong>アクセシビリティ重視なら <code>dnd-kit</code> を採用</strong></li>
<li><code>dataTransfer.setData('text/uri-list', url)</code> で URL D&D に対応</li>
<li>セキュリティ上、別オリジン間でのファイルパス取得は禁止</li>
</ul>
<h2>モダンライブラリの選び方</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e8f5e9"><th>ライブラリ</th><th>用途</th><th>特徴</th></tr></thead>
<tbody>
<tr><td><strong>SortableJS</strong></td><td>リスト並べ替え</td><td>Vanilla JS、フレームワーク非依存、軽量</td></tr>
<tr><td><strong>dnd-kit</strong></td><td>React</td><td>アクセシビリティ良好、Tree / Kanban 対応、ヘッドレス</td></tr>
<tr><td><strong>react-dnd</strong></td><td>React</td><td>古参、HTML5 backend と touch backend を切替</td></tr>
<tr><td><strong>vue.draggable.next</strong></td><td>Vue 3</td><td>SortableJS のラッパー</td></tr>
<tr><td><strong>Interact.js</strong></td><td>汎用</td><td>リサイズ・ジェスチャ込み、低レイヤ</td></tr>
</tbody>
</table>
<h2>FAQ</h2>
<p><strong>Q: <code>drop</code> イベントが発火しません</strong><br />
A: 99% は <code>dragover</code> の <code>preventDefault()</code> 忘れです。<code>dragenter</code> でも入れておくと確実。</p>
<p><strong>Q: スマホでドラッグできません</strong><br />
A: HTML5 DnD はモバイルでは動作が不安定です。SortableJS(touch サポートあり)や Pointer Events を用いた自前実装に切り替えてください。</p>
<p><strong>Q: ドラッグ中の見た目を変えたい</strong><br />
A: <code>dragstart</code> で <code>e.dataTransfer.setDragImage(canvasEl, 0, 0)</code>。canvas に好きな画像を描いて差し替えます。</p>