Skip to content

Commit 30c0c7c

Browse files
committed
Rewrite file DnD
1 parent f6846d8 commit 30c0c7c

File tree

3 files changed

+150
-51
lines changed

3 files changed

+150
-51
lines changed

files/en-us/web/api/html_drag_and_drop_api/drag_data_store/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ page-type: guide
66

77
{{DefaultAPISidebar("HTML Drag and Drop API")}}
88

9-
The {{domxref("DragEvent")}} interface has a {{domxref("DragEvent.dataTransfer","dataTransfer")}} property, which is a {{domxref("DataTransfer")}} object. {{domxref("DataTransfer")}} objects represent the main context of the drag operation, and it stays consistent across the firing of different events. It includes the [drag data](/en-US/docs/Web/API/HTML_Drag_and_Drop_API#drag_data_store), drag image, drop effect, etc. This article focuses on the _data store_ part of the `dataTransfer`.
9+
The {{domxref("DragEvent")}} interface has a {{domxref("DragEvent.dataTransfer","dataTransfer")}} property, which is a {{domxref("DataTransfer")}} object. {{domxref("DataTransfer")}} objects represent the main context of the drag operation, and it stays consistent across the firing of different events. It includes the [drag data](/en-US/docs/Web/API/HTML_Drag_and_Drop_API#drag_data_store), [drag image](/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#setting_the_drag_feedback_image), [drop effect](/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#drop_effects), etc. This article focuses on the _data store_ part of the `dataTransfer`.
1010

1111
## DataTransfer, DataTransferItem, and DataTransferItemList
1212

files/en-us/web/api/html_drag_and_drop_api/drag_operations/index.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ In this example, we add a listener for the {{domxref("HTMLElement/dragstart_even
3030

3131
```js
3232
const draggableElement = document.querySelector('p[draggable="true"]');
33-
draggableElement.addEventListener("dragstart", (event) =>
34-
event.dataTransfer.setData("text/plain", "This text may be dragged"),
35-
);
33+
draggableElement.addEventListener("dragstart", (event) => {
34+
event.dataTransfer.setData("text/plain", "This text may be dragged");
35+
});
3636
```
3737

3838
You could also listen to a higher ancestor as drag events bubble up as most other events do. For this reason, it is common to also check the event's target, so that dragging a selection contained within this element does not trigger the `setData` (although selecting text within the element is hard, it is not impossible):
@@ -304,13 +304,15 @@ The {{domxref("HTMLElement/dragover_event", "dragover")}} event will fire at the
304304

305305
Finally, the {{domxref("HTMLElement/dragleave_event", "dragleave")}} event will fire at an element when the drag leaves the element. This is the time when you should remove any insertion markers or highlighting. You do not need to cancel this event. The {{domxref("HTMLElement/dragleave_event", "dragleave")}} event will always fire, even if the drag is cancelled, so you can always ensure that any insertion point cleanup can be done during this event.
306306

307+
For a practical example of using these events, see our [Kanban board example](/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Kanban_board#inserting_at_a_particular_location).
308+
307309
## Performing a drop
308310

309311
When the user releases the mouse, the drag and drop operation ends.
310312

311313
In order for the drop to be _potentially successful_, the drop must happen over a valid [drop target](#dragging_over_elements_and_specifying_drop_targets), and the `dropEffect` must not be `none` at the time of mouse release. Otherwise, the drop operation is considered [failed](#a_failed_drop).
312314

313-
If the drop is potentially successful, a {{domxref("HTMLElement/drop_event", "drop")}} event is fired on the drop target. You need to cancel this event using `preventDefault()` in order for the drop to be considered actually successful. Otherwise, the drop is also considered successful if the drop was dropping text (the data contains a `text/plain` item) into an editable text field. In this case, the text is inserted into the field (either at the cursor position or at the end, depending on platform conventions) and, if the `dropEffect` is `move` while the source is a selection within an editable region, the source is removed. Otherwise, for all other drag data and drop targets, the drop is also considered failed by default.
315+
If the drop is potentially successful, a {{domxref("HTMLElement/drop_event", "drop")}} event is fired on the drop target. You need to cancel this event using `preventDefault()` in order for the drop to be considered actually successful. Otherwise, the drop is also considered successful if the drop was dropping text (the data contains a `text/plain` item) into an editable text field. In this case, the text is inserted into the field (either at the cursor position or at the end, depending on platform conventions) and, if the `dropEffect` is `move` while the source is a selection within an editable region, the source is removed. Otherwise, for all other drag data and drop targets, the drop is considered failed.
314316

315317
During the {{domxref("HTMLElement/drop_event", "drop")}} event, you should retrieve that data that was dropped from the event and insert it at the drop location. You can use the {{domxref("DataTransfer.dropEffect","dropEffect")}} property to determine which drag operation was desired. The `drop` event is the only time when you can read the drag data store, other than `dragstart`.
316318

files/en-us/web/api/html_drag_and_drop_api/file_drag_and_drop/index.md

Lines changed: 143 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,91 +6,188 @@ page-type: guide
66

77
{{DefaultAPISidebar("HTML Drag and Drop API")}}
88

9-
HTML Drag and Drop interfaces enable web applications to drag and drop files on a web page. This document describes how an application can accept one or more files that are dragged from the underlying platform's _file manager_ and dropped on a web page.
9+
As mentioned on [the landing page](/en-US/docs/Web/API/HTML_Drag_and_Drop_API#concepts_and_usage), the Drag and Drop API simultaneously models three use cases: dragging elements within a page, dragging data out of a page, and dragging data into a page. This tutorial demonstrates the third use case: dragging data into a page. We will be implementing a basic drop zone that admits dropping image files from the user's operation system file explorer and displays them on the page. For users who can't or don't want to use drag & drop, we also provide the alternative functionality of file selection via an `<input>` element.
1010

11-
The main steps to drag and drop are to define a _drop zone_ (i.e., a target element for the file drop) and to define event handlers for the {{domxref("HTMLElement/drop_event", "drop")}} and {{domxref("HTMLElement/dragover_event", "dragover")}} events. These steps are described below, including example code snippets.
11+
## Basic page layout
1212

13-
## Define the drop zone
14-
15-
The HTML defines the drop zone as a {{htmlelement("div")}}, and an output region ({{htmlelement("pre")}}) to be populated later.
13+
Because we want to allow normal `<input>` file selection as well, it makes sense for the drop zone to be backed by an `<input>` element so that we can simultaneously drag into it and click on it. We take advantage of a common trick, which is to make the `<input>` invisible, and use its associated {{HTMLElement("label")}} to interact with the user instead, because `<label>` elements are much easier to style. We also add the elements for previewing the dropped images.
1614

1715
```html live-sample___file-dnd
18-
<div id="drop-zone">
19-
<p>Drag one or more files to this <i>drop zone</i>.</p>
20-
</div>
21-
<pre id="output"></pre>
16+
<label id="drop-zone">
17+
Drop images here, or click to upload.
18+
<input type="file" id="file-input" multiple accept="image/*" />
19+
</label>
20+
<ul id="preview"></ul>
21+
<button id="clear-btn">Clear</button>
22+
```
23+
24+
We style the label element to visually indicate the element is a drop zone, and hide the file input.
25+
26+
```css live-sample___file-dnd
27+
body {
28+
font-family: "Arial", sans-serif;
29+
}
30+
31+
#drop-zone {
32+
display: flex;
33+
align-items: center;
34+
justify-content: center;
35+
width: 500px;
36+
max-width: 100%;
37+
height: 200px;
38+
padding: 1em;
39+
border: 1px solid #cccccc;
40+
border-radius: 4px;
41+
color: slategray;
42+
cursor: pointer;
43+
}
44+
45+
#file-input {
46+
display: none;
47+
}
48+
49+
#preview {
50+
width: 500px;
51+
max-width: 100%;
52+
display: flex;
53+
flex-direction: column;
54+
gap: 0.5em;
55+
list-style: none;
56+
padding: 0;
57+
}
58+
59+
#preview li {
60+
display: flex;
61+
align-items: center;
62+
gap: 0.5em;
63+
margin: 0;
64+
width: 100%;
65+
height: 100px;
66+
}
67+
68+
#preview img {
69+
width: 100px;
70+
height: 100px;
71+
object-fit: cover;
72+
}
2273
```
2374

24-
As the _target element_, it listens to the {{domxref("HTMLElement/drop_event", "drop")}} event to process the dropped file.
75+
By virtue of us using the `<label>` and `<input>` elements, no additional JavaScript is needed to implement the file selection UX. We now focus on file dropping and the subsequent processing of the dropped files.
76+
77+
## Declaring the drop target
78+
79+
Our drop target is the `<label>` element. As the _target element_, it listens to the {{domxref("HTMLElement/drop_event", "drop")}} event to process the dropped file.
2580

2681
```js live-sample___file-dnd
2782
const dropZone = document.getElementById("drop-zone");
28-
const output = document.getElementById("output");
2983

3084
dropZone.addEventListener("drop", dropHandler);
3185
```
3286

33-
In order for the `drop` event to fire, the element must also cancel the {{domxref("HTMLElement/dragover_event", "dragover")}} event. Here, we cancel the event on `window` (which would also cancel the event fired on `dropZone` as it bubbles up), because we also want to listen for the `drop` event on `window` to prevent the default browser action of opening the file when it was not dropped into the drop zone.
87+
For file dropping, the browser may process them by default (such as opening or downloading the file) even when the file is not dropped into a valid drop target. To prevent this behavior, we also need to listen for the `drop` event on `window` and cancel it. We take care to only handle the event only if a file is being dragged; if it's something else, such as a link, we still use the default behavior. If the dragged item is a non-image file, we still handle the event, but provide feedback to the user that it is not allowed.
3488

3589
```js live-sample___file-dnd
36-
window.addEventListener("dragover", (e) => {
37-
e.preventDefault();
38-
});
3990
window.addEventListener("drop", (e) => {
40-
e.preventDefault();
91+
if ([...e.dataTransfer.items].some((item) => item.kind === "file")) {
92+
e.preventDefault();
93+
}
4194
});
4295
```
4396

44-
Lastly, an application may want to style the drop target element to visually indicate the element is a drop zone. In this example, the drop target element uses the following styling:
97+
In order for the `drop` event to fire, the element must also cancel the {{domxref("HTMLElement/dragover_event", "dragover")}} event. Because we are listening for `drop` on `window`, we need to cancel the `dragover` event for the whole `window` as well. We also set {{domxref("DataTransfer.dropEffect")}} to `none` if the file is not an image or not dragged to the correct place.
4598

46-
```css live-sample___file-dnd
47-
#drop-zone {
48-
border: 5px solid blue;
49-
width: 200px;
50-
height: 100px;
51-
}
52-
```
99+
```js live-sample___file-dnd
100+
dropZone.addEventListener("dragover", (e) => {
101+
const fileItems = [...e.dataTransfer.items].filter(
102+
(item) => item.kind === "file",
103+
);
104+
if (fileItems.length > 0) {
105+
e.preventDefault();
106+
if (fileItems.some((item) => item.type.startsWith("image/"))) {
107+
e.dataTransfer.dropEffect = "copy";
108+
} else {
109+
e.dataTransfer.dropEffect = "none";
110+
}
111+
}
112+
});
53113

54-
```css hidden live-sample___file-dnd
55-
div {
56-
margin: 0em;
57-
padding: 2em;
58-
}
114+
window.addEventListener("dragover", (e) => {
115+
const fileItems = [...e.dataTransfer.items].filter(
116+
(item) => item.kind === "file",
117+
);
118+
if (fileItems.length > 0) {
119+
e.preventDefault();
120+
if (!dropZone.contains(e.target)) {
121+
e.dataTransfer.dropEffect = "none";
122+
}
123+
}
124+
});
59125
```
60126

61127
> [!NOTE]
62128
> {{domxref("HTMLElement/dragstart_event", "dragstart")}} and {{domxref("HTMLElement/dragend_event", "dragend")}} events are not fired when dragging a file into the browser from the OS. To detect when OS files are dragged into the browser, use {{domxref("HTMLElement/dragenter_event", "dragenter")}} and {{domxref("HTMLElement/dragleave_event", "dragleave")}}.
63129
> This means that it is not possible to use {{domxref("DataTransfer.setDragImage","setDragImage()")}} to apply a custom drag image/cursor overlay when dragging files from the OS — because the drag data store can only be modified in the {{domxref("HTMLElement/dragstart_event", "dragstart")}} event. This also applies to {{domxref("DataTransfer.setData","setData()")}}.
64130
65-
## Process the drop
66-
67-
The {{domxref("HTMLElement/drop_event", "drop")}} event is fired when the user drops the file(s). In the following drop handler, the {{domxref("DataTransferItem.getAsFile","getAsFile()")}} method is used to access each file. This example shows how to write the name of each dragged file to the console. In a _real_ application, an application may want to process a file using the [File API](/en-US/docs/Web/API/File_API).
131+
## Processing the drop
68132

69-
Note that in this example, any drag item that is not a file is ignored.
133+
Now we implement the `dropHandler` by using the {{domxref("DataTransferItem.getAsFile","getAsFile()")}} method to access each file. Then your application can decide how to process this file using the [File API](/en-US/docs/Web/API/File_API). Here we just display them on the page; in practice, you probably want to eventually upload them to the server as well.
70134

71135
```js live-sample___file-dnd
136+
const preview = document.getElementById("preview");
137+
138+
function displayImages(files) {
139+
for (const file of files) {
140+
if (file.type.startsWith("image/")) {
141+
const li = document.createElement("li");
142+
const img = document.createElement("img");
143+
img.src = URL.createObjectURL(file);
144+
img.alt = file.name;
145+
li.appendChild(img);
146+
li.appendChild(document.createTextNode(file.name));
147+
preview.appendChild(li);
148+
}
149+
}
150+
}
151+
72152
function dropHandler(ev) {
73-
// Prevent default behavior (Prevent file from being opened)
74153
ev.preventDefault();
75-
let result = "";
76-
// Use DataTransferItemList interface to access the file(s)
77-
[...ev.dataTransfer.items].forEach((item, i) => {
78-
// If dropped items aren't files, reject them
79-
if (item.kind === "file") {
80-
const file = item.getAsFile();
81-
result += `• file[${i}].name = ${file.name}\n`;
82-
}
83-
});
84-
output.textContent = result;
154+
const files = [...ev.dataTransfer.items]
155+
.map((item) => item.getAsFile())
156+
.filter((file) => file);
157+
displayImages(files);
85158
}
86159
```
87160

161+
## Adding the same behavior to the input
162+
163+
The above is the whole data flow for the drag and drop; now we need to wire the `displayImages()` function to the file input as well.
164+
165+
```js live-sample___file-dnd
166+
const fileInput = document.getElementById("file-input");
167+
fileInput.addEventListener("change", (e) => {
168+
displayImages(e.target.files);
169+
});
170+
```
171+
172+
## Clear button
173+
174+
Finally we add a way to clear the preview area. We use {{domxref("URL.revokeObjectURL_static","URL.revokeObjectURL()")}} to release the memory used by the image objects.
175+
176+
```js live-sample___file-dnd
177+
const clearBtn = document.getElementById("clear-btn");
178+
clearBtn.addEventListener("click", () => {
179+
for (const img of preview.querySelectorAll("img")) {
180+
URL.revokeObjectURL(img.src);
181+
}
182+
preview.textContent = "";
183+
});
184+
```
185+
88186
## Result
89187

90-
{{EmbedLiveSample("file-dnd", "", 300)}}
188+
{{EmbedLiveSample("file-dnd", "", 500)}}
91189

92190
## See also
93191

94192
- [HTML Drag and Drop API](/en-US/docs/Web/API/HTML_Drag_and_Drop_API)
95193
- [Drag Operations](/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations)
96-
- [HTML Living Standard: Drag and Drop](https://html.spec.whatwg.org/multipage/interaction.html#dnd)

0 commit comments

Comments
 (0)