|
2 | 2 | title: ファイルのドラッグ & ドロップ |
3 | 3 | slug: Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop |
4 | 4 | l10n: |
5 | | - sourceCommit: 0230ecc4418a1e52bca6b4d03c4eb794f90d04f1 |
| 5 | + sourceCommit: 8285d415db211ae9efe04752d9dab1b574450ee8 |
6 | 6 | --- |
7 | 7 |
|
8 | 8 | {{DefaultAPISidebar("HTML Drag and Drop API")}} |
9 | 9 |
|
10 | | -HTML ドラッグ & ドロップインターフェイスは、ウェブアプリケーションでファイルをウェブページにドラッグ & ドロップできるようにするものです。この記事では、基礎となるプラットフォームのファイルマネージャーからドラッグされ、ウェブページにドロップされた 1 つまたは複数のファイルを、アプリケーションで受け入れる方法について記述しています。 |
| 10 | +[ランディングページ](/ja/docs/Web/API/HTML_Drag_and_Drop_API#概念と使用法)で述べたように、ドラッグ&ドロップ API は、ページ内での要素のドラッグ、ページからのデータのドラッグ、ページへのデータのドラッグの 3 つの用途を同時にモデル化します。このチュートリアルでは、 3 つ目の用途である「ページへのデータのドラッグ」を実演します。ユーザーがオペレーティングシステムのファイルエクスプローラーから画像ファイルをドロップし、ページ上に表示できる基本的なドロップゾーンを実装します。ドラッグ&ドロップが利用できない、または利用したくないユーザー向けに、`<input>` 要素によるファイル選択の代替機能も提供します。 |
11 | 11 |
|
12 | | -ドラッグ & ドロップの主な手順は、ドロップゾーン(ファイルドロップの対象要素)を定義することと、 {{domxref("HTMLElement/drop_event", "drop")}} および {{domxref("HTMLElement/dragover_event", "dragover")}} イベントのイベントハンドラーを定義することです。これらの手順は、コード例を含め、下記で記述します。完全なソースコードは [MDN のドラッグ & ドロップリポジトリー](https://github.com/mdn/dom-examples/tree/main/drag-and-drop)で利用できます(プルリクエストや issue を歓迎します)。 |
| 12 | +## 基本的なページレイアウト |
13 | 13 |
|
14 | | -なお、 {{domxref("HTML_Drag_and_Drop_API","HTML ドラッグ & ドロップ", "", 1)}}では、ファイルのドラッグ & ドロップに対応するために 2 つの異なる形の API を定義しています。一方の API は {{domxref("DataTransfer")}} インターフェイスで、もう一方の API は {{domxref("DataTransferItem")}} と {{domxref("DataTransferItemList")}} インターフェイスです。この例では、両方の API の使用方法を説明します (そして、Gecko 固有のインターフェイスは一切使用しません)。 |
| 14 | +通常の `<input>` によるファイル選択も許可したいので、ドロップゾーンは `<input>` 要素で実装するのが合理的です。これにより、ドラッグでの投入とクリック操作を同時に行うことができます。一般的な手法として、`<input>` を非表示にし、代わりにスタイル設定が容易な {{HTMLElement("label")}} 要素と連動させます。また、ドロップされた画像のプレビュー表示用要素も追加します。 |
15 | 15 |
|
16 | | -## ドロップゾーンの定義 |
| 16 | +```html live-sample___file-dnd |
| 17 | +<label id="drop-zone"> |
| 18 | + ここに画像をドロップ、またはクリックしてアップロードしてください。 |
| 19 | + <input type="file" id="file-input" multiple accept="image/*" /> |
| 20 | +</label> |
| 21 | +<ul id="preview"></ul> |
| 22 | +<button id="clear-btn">クリア</button> |
| 23 | +``` |
17 | 24 |
|
18 | | -{{domxref("HTMLElement/drop_event", "drop")}} イベントの対象となる要素には、 `ondrop` イベントハンドラーが必要です。以下のコードでは、 {{HTMLelement("div")}} 要素を使用してこの処理を行う方法を示しています。 |
| 25 | +ラベル要素をスタイル設定し、視覚的にドロップゾーンであることを示し、ファイル入力は非表示にします。 |
19 | 26 |
|
20 | | -```html |
21 | | -<div id="drop_zone" ondrop="dropHandler(event);"> |
22 | | - <p>Drag one or more files to this <i>drop zone</i>.</p> |
23 | | -</div> |
24 | | -``` |
| 27 | +```css live-sample___file-dnd |
| 28 | +body { |
| 29 | + font-family: "Arial", sans-serif; |
| 30 | +} |
25 | 31 |
|
26 | | -通常、アプリケーションでは、ドロップ対象の要素に {{domxref("HTMLElement/dragover_event", "dragover")}} イベントハンドラーを記述し、そのハンドラーによって、ブラウザーの既定のドラッグ動作をオフにします。このハンドラーを追加するには、 {{domxref("HTMLElement.dragover_event","ondragover")}} イベントハンドラーを記載する必要があります。 |
| 32 | +#drop-zone { |
| 33 | + display: flex; |
| 34 | + align-items: center; |
| 35 | + justify-content: center; |
| 36 | + width: 500px; |
| 37 | + max-width: 100%; |
| 38 | + height: 200px; |
| 39 | + padding: 1em; |
| 40 | + border: 1px solid #cccccc; |
| 41 | + border-radius: 4px; |
| 42 | + color: slategray; |
| 43 | + cursor: pointer; |
| 44 | +} |
27 | 45 |
|
28 | | -```html |
29 | | -<div |
30 | | - id="drop_zone" |
31 | | - ondrop="dropHandler(event);" |
32 | | - ondragover="dragOverHandler(event);"> |
33 | | - <p>Drag one or more files to this <i>drop zone</i>.</p> |
34 | | -</div> |
35 | | -``` |
| 46 | +#file-input { |
| 47 | + display: none; |
| 48 | +} |
| 49 | + |
| 50 | +#preview { |
| 51 | + width: 500px; |
| 52 | + max-width: 100%; |
| 53 | + display: flex; |
| 54 | + flex-direction: column; |
| 55 | + gap: 0.5em; |
| 56 | + list-style: none; |
| 57 | + padding: 0; |
| 58 | +} |
36 | 59 |
|
37 | | -最後に、アプリケーションはドロップ先の要素のスタイルを設定して、要素がドロップゾーンであることを視覚的に示したい場合があります。この例では、 drop ドロップ先の要素は以下のスタイル設定を使用します。 |
| 60 | +#preview li { |
| 61 | + display: flex; |
| 62 | + align-items: center; |
| 63 | + gap: 0.5em; |
| 64 | + margin: 0; |
| 65 | + width: 100%; |
| 66 | + height: 100px; |
| 67 | +} |
38 | 68 |
|
39 | | -```css |
40 | | -#drop_zone { |
41 | | - border: 5px solid blue; |
42 | | - width: 200px; |
| 69 | +#preview img { |
| 70 | + width: 100px; |
43 | 71 | height: 100px; |
| 72 | + object-fit: cover; |
44 | 73 | } |
45 | 74 | ``` |
46 | 75 |
|
47 | | -> [!NOTE] |
48 | | -> {{domxref("HTMLElement/dragstart_event", "dragstart")}} および {{domxref("HTMLElement/dragend_event", "dragend")}} イベントは、 OS からブラウザーへファイルをドラッグしているときには発生しません。 OS のファイルがブラウザーへドラッグされてきたことを検出するには、 {{domxref("HTMLElement/dragenter_event", "dragenter")}} および {{domxref("HTMLElement/dragleave_event", "dragleave")}} を使用してください。 |
| 76 | +`<label>` 要素と `<input>` 要素を使用することで、ファイル選択 UX の実装に追加の JavaScript は不要になります。次に、ファイルのドロップ操作と、ドロップされたファイルのその後の処理に焦点を当てます。 |
49 | 77 |
|
50 | | -## ドロップの処理 |
| 78 | +## ドロップターゲットの宣言 |
51 | 79 |
|
52 | | -{{domxref("HTMLElement/drop_event", "drop")}} イベントは、ユーザーがファイルをドロップしたときに発行されます。以下のドロップハンドラーでは、ブラウザーが {{domxref("DataTransferItemList")}} インターフェイスを使用している場合は {{domxref("DataTransferItem.getAsFile","getAsFile()")}} メソッドを使用して各ファイルにアクセスし、そうでない場合は {{domxref("DataTransfer")}} インターフェイスの {{domxref("DataTransfer.files","files")}} プロパティを使用して各ファイルにアクセスしています。 |
| 80 | +ドロップターゲットは `<label>` 要素です。ターゲット要素として、 {{domxref("HTMLElement/drop_event", "drop")}} イベントを監視し、ドロップされたファイルを処理します。 |
53 | 81 |
|
54 | | -この例では、ドラッグしたそれぞれのファイル名をコンソールに書き出す方法を説明します。実際のアプリケーションでは、 {{domxref("File", "File API")}} を使用してファイルを処理したいかもしれません。 |
| 82 | +```js live-sample___file-dnd |
| 83 | +const dropZone = document.getElementById("drop-zone"); |
55 | 84 |
|
56 | | -この例では、ファイルでないドラッグアイテムは無視されることに注意してください。 |
| 85 | +dropZone.addEventListener("drop", dropHandler); |
| 86 | +``` |
57 | 87 |
|
58 | | -```js |
59 | | -function dropHandler(ev) { |
60 | | - console.log("File(s) dropped"); |
| 88 | +ファイルドロップの場合、ファイルが有効なドロップ先へドロップされなくても、ブラウザーがデフォルトで処理(ファイルの開く/ダウンロードなど)を行うことがあります。この動作を防ぐため、`window` の `drop` イベントを監視し、キャンセルする必要があります。ファイルがドラッグされている場合のみイベントを処理するように注意します。リンクなど他の要素の場合は、デフォルトの動作をそのまま使用します。ドラッグされたアイテムが画像以外のファイルの場合、イベントは処理しますが、許可されていないことをユーザーにフィードバックします。 |
61 | 89 |
|
62 | | - // 既定の動作で防ぐ(ファイルが開かれないようにする) |
63 | | - ev.preventDefault(); |
| 90 | +```js live-sample___file-dnd |
| 91 | +window.addEventListener("drop", (e) => { |
| 92 | + if ([...e.dataTransfer.items].some((item) => item.kind === "file")) { |
| 93 | + e.preventDefault(); |
| 94 | + } |
| 95 | +}); |
| 96 | +``` |
64 | 97 |
|
65 | | - if (ev.dataTransfer.items) { |
66 | | - // DataTransferItemList インターフェイスを使用して、ファイルにアクセスする |
67 | | - [...ev.dataTransfer.items].forEach((item, i) => { |
68 | | - // ドロップしたものがファイルでない場合は拒否する |
69 | | - if (item.kind === "file") { |
70 | | - const file = item.getAsFile(); |
71 | | - console.log(`… file[${i}].name = ${file.name}`); |
72 | | - } |
73 | | - }); |
74 | | - } else { |
75 | | - // DataTransfer インターフェイスを使用してファイルにアクセスする |
76 | | - [...ev.dataTransfer.files].forEach((file, i) => { |
77 | | - console.log(`… file[${i}].name = ${file.name}`); |
78 | | - }); |
| 98 | +`drop` イベントを発生させるには、要素が {{domxref("HTMLElement/dragover_event", "dragover")}} イベントもキャンセルする必要があります。`window` 上で `drop` を監視しているため、`window` 全体に対する `dragover` イベントもキャンセルする必要があります。また、ファイルが画像でない場合や正しい場所にドラッグされていない場合、 {{domxref("DataTransfer.dropEffect")}} を `none` に設定します。 |
| 99 | + |
| 100 | +```js live-sample___file-dnd |
| 101 | +dropZone.addEventListener("dragover", (e) => { |
| 102 | + const fileItems = [...e.dataTransfer.items].filter( |
| 103 | + (item) => item.kind === "file", |
| 104 | + ); |
| 105 | + if (fileItems.length > 0) { |
| 106 | + e.preventDefault(); |
| 107 | + if (fileItems.some((item) => item.type.startsWith("image/"))) { |
| 108 | + e.dataTransfer.dropEffect = "copy"; |
| 109 | + } else { |
| 110 | + e.dataTransfer.dropEffect = "none"; |
| 111 | + } |
79 | 112 | } |
80 | | -} |
| 113 | +}); |
| 114 | + |
| 115 | +window.addEventListener("dragover", (e) => { |
| 116 | + const fileItems = [...e.dataTransfer.items].filter( |
| 117 | + (item) => item.kind === "file", |
| 118 | + ); |
| 119 | + if (fileItems.length > 0) { |
| 120 | + e.preventDefault(); |
| 121 | + if (!dropZone.contains(e.target)) { |
| 122 | + e.dataTransfer.dropEffect = "none"; |
| 123 | + } |
| 124 | + } |
| 125 | +}); |
81 | 126 | ``` |
82 | 127 |
|
83 | | -## ブラウザー既定のドラッグ動作を阻止する |
| 128 | +> [!NOTE] |
| 129 | +> {{domxref("HTMLElement/dragstart_event", "dragstart")}} および {{domxref("HTMLElement/dragend_event", "dragend")}} イベントは、OS からブラウザーへファイルをドラッグする際に発生しません。OS のファイルがブラウザーにドラッグされたことを検出するには、 {{domxref("HTMLElement/dragenter_event", "dragenter")}} および {{domxref("HTMLElement/dragleave_event", "dragleave")}} を使用してください。 |
| 130 | +> つまり、OS からファイルをドラッグする際には、{{domxref("DataTransfer.setDragImage","setDragImage()")}} を使用してカスタムのドラッグ画像やカーソルオーバーレイを適用することはできません。これは、ドラッグデータストアが {{domxref("HTMLElement/dragstart_event", "dragstart")}} イベント内でのみ変更可能であるためです。この制限は {{domxref("DataTransfer.setData","setData()")}} にも適用されます。 |
84 | 131 |
|
85 | | -以下の {{domxref("HTMLElement/dragover_event", "dragover")}} イベントハンドラーは {{domxref("Event.preventDefault", "preventDefault()")}} を呼び出して、ブラウザーの既定のドラッグ & ドロップハンドラーをオフにしています。 |
| 132 | +## ドロップの処理 |
86 | 133 |
|
87 | | -```js |
88 | | -function dragOverHandler(ev) { |
89 | | - console.log("File(s) in drop zone"); |
| 134 | +次に、各ファイルにアクセスするために {{domxref("DataTransferItem.getAsFile","getAsFile()")}} メソッドを使用して `dropHandler` を実装します。その後、アプリケーションは[ファイル API](/ja/docs/Web/API/File_API) を使用してこのファイルをどのように処理するかを決定できます。ここでは単にページ上に表示していますが、実際には最終的にサーバーへアップロードすることも必要になるでしょう。 |
| 135 | + |
| 136 | +```js live-sample___file-dnd |
| 137 | +const preview = document.getElementById("preview"); |
| 138 | + |
| 139 | +function displayImages(files) { |
| 140 | + for (const file of files) { |
| 141 | + if (file.type.startsWith("image/")) { |
| 142 | + const li = document.createElement("li"); |
| 143 | + const img = document.createElement("img"); |
| 144 | + img.src = URL.createObjectURL(file); |
| 145 | + img.alt = file.name; |
| 146 | + li.appendChild(img); |
| 147 | + li.appendChild(document.createTextNode(file.name)); |
| 148 | + preview.appendChild(li); |
| 149 | + } |
| 150 | + } |
| 151 | +} |
90 | 152 |
|
91 | | - // 既定の動作で防ぐ(ファイルが開かれないようにする) |
| 153 | +function dropHandler(ev) { |
92 | 154 | ev.preventDefault(); |
| 155 | + const files = [...ev.dataTransfer.items] |
| 156 | + .map((item) => item.getAsFile()) |
| 157 | + .filter((file) => file); |
| 158 | + displayImages(files); |
93 | 159 | } |
94 | 160 | ``` |
95 | 161 |
|
| 162 | +## input に同じ動作を追加 |
| 163 | + |
| 164 | +上記がドラッグ&ドロップの全データフローです。次に、`displayImages()` 関数をファイル入力欄にも接続する必要があります。 |
| 165 | + |
| 166 | +```js live-sample___file-dnd |
| 167 | +const fileInput = document.getElementById("file-input"); |
| 168 | +fileInput.addEventListener("change", (e) => { |
| 169 | + displayImages(e.target.files); |
| 170 | +}); |
| 171 | +``` |
| 172 | + |
| 173 | +## クリアボタン |
| 174 | + |
| 175 | +最後にプレビュー領域をクリアする方法を追加します。 {{domxref("URL.revokeObjectURL_static","URL.revokeObjectURL()")}} を使用して画像オブジェクトが使用していたメモリを解放します。 |
| 176 | + |
| 177 | +```js live-sample___file-dnd |
| 178 | +const clearBtn = document.getElementById("clear-btn"); |
| 179 | +clearBtn.addEventListener("click", () => { |
| 180 | + for (const img of preview.querySelectorAll("img")) { |
| 181 | + URL.revokeObjectURL(img.src); |
| 182 | + } |
| 183 | + preview.textContent = ""; |
| 184 | +}); |
| 185 | +``` |
| 186 | + |
| 187 | +## 結果 |
| 188 | + |
| 189 | +{{EmbedLiveSample("file-dnd", "", 500)}} |
| 190 | + |
96 | 191 | ## 関連情報 |
97 | 192 |
|
98 | | -- [HTML ドラッグ & ドロップ API (概要)](/ja/docs/Web/API/HTML_Drag_and_Drop_API) |
| 193 | +- [HTML ドラッグ&ドロップ API](/ja/docs/Web/API/HTML_Drag_and_Drop_API) |
99 | 194 | - [ドラッグ操作](/ja/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations) |
100 | | -- [HTML Living Standard: Drag and Drop](https://html.spec.whatwg.org/multipage/interaction.html#dnd) |
|
0 commit comments