This is a collection of knowledge I have built up regarding browser powered drag and drop functionality
- timing: once as drag is starting
event.target
: draggableElement
- 📸 Called before drag preview image is taken
- 📸 You can set the drag preview with
event.dataTransfer.setDragImage()
- You can set data for the drag (eg
event.dataTransfer.setData(format, data)
) - 💻 for iOS15 you must set a drag operations drag data (
event.dataTransfer.setData('application/draggable-id', 'hi')
) - You can set the drag effects allowed for a drag (eg
event.dataTransfer.effectAllowed = 'copy';
) - 🚫 call
event.preventDefault()
to cancel dragging (useful for powering drag handles)
- timing: continually during a drag (throttled)
event.target
: draggableElement
- can be used to get information such as
clientX,clientY
- could also be used for edge detection (or could use dragover)
- pretty useless right now as firefox sets
clientX,clientY
to0
- not helpful if draggable
Element
is removed during a drag
- timing: fired when entering a new element
event.target
: potential drop targetElement
- called before
"dragleave"
is called on the old element 😭 - 🩸 must call
event.preventDefault()
in order forElement
to be a valid drop target - ℹ️ is called on the initial draggable
Element
- timing: fired when dragging out of an element.
event.target
: potential drop targetElement
- called after
dragenter
is called on the new element 😭
- timing: continually while over an
Element
(throttled) event.target
: potential drop targetElement
- 🩸 must call
event.preventDefault()
in order forElement
to be a valid drop target
- timing: when a valid drop target is dropped on
event.target
: drop targetElement
- will not fire if no valid drop target
- 🩸 must call
event.preventDefault()
to prevent the default browser drop behaviour
- timing: when drag has finished, whether it was successful or not
event.target
: draggableElement
- occurs after
drop
When a drag operation is canceled:
- a
dragleave
event fires on the existing drop target - the
dataTransfer.dropEffect
is set tonone
(but not on FF 😢)
The browser will automatically take a snapshot image of the Element
being dragged. This snapshot is what the user drags around. The snapshot is often referred to as a "ghost", "drag preview", "drag image" and more.
You can set a custom drag image by using the DataTransfer.setDragImage()
function.
Options:
- an
<img>
(in the document, or not in the document). Note: The image must be decoded before"dragstart"
in order to be used - a
<canvas>
- a visible
Element
.
Commonly people want the dragging image to be a minor modification of the draggable Element
(eg the same Element
but with a different background color). So to do this you need to provide DataTransfer.setDragImage()
with a visible Element
(option 3).
Things you need to do:
- render the draggable
Element
somewhere else - modify that
Element
with the changes you want - somehow make the new
Element
visible to the browser, but not to a user
This technique is a trainwreck. It is complicated and has a poor cross browser story (from my minor exploration).
I found a (novel?) technique that allows simple and cheap creation of custom drag previews 🤩
- Browsers execute tasks (javascript) in a task queue
- Every ~1/60th of a second, after the task queue is empty, the browser will draw to the screen (style, layout, paint)
requestAnimationFrame
schedules some javascript to run after the task queue, and just before drawing
- During a
dragstart
event, make any visual changes to the draggableElement
that you want to be present in the drag preview image (eg a change in background color).
element.addEventListener('dragstart', () => {
element.classList.add('drag-preview');
});
- 📸 After
dragstart
is finished, the browser will take a 'photo' of the element when thedragstart
event is done
this behaviour is defined in the drag and drop processing model
- In your
dragstart
event listener, schedule a future animation frame to revert the changes that you made to the draggableElement
. Because an animation frame runs before drawing (style, layout, paint) the user will never see the visual changes you made to the draggableElement
in step 1
element.addEventListener('dragstart', () => {
element.classList.add('drag-preview');
requestAnimationFrame(() => {
element.classList.remove('drag-preview');
});
});
- 👩🎨 Make visual changes to draggable
Element
insidedragstart
that you want to be on your drag preview - 📸 The browser will take a 'photo' of the element to be used as the drag preview
- ⎌ in the next animation frame revert the changes in step 1
If there is a long task in dragstart
will that cause the styles applied in dragstart
to be visible on the draggable Element
?
Nope! A long task does not cancel the next animation frames (or paint). A long task will push back the next animation frames and browser paints.
Browser generated drag previews do not work well with CSS transform
s.
- Poor cross browser experience when a draggable
Element
has atransform
- Poor cross browser experience when adding a
transform
duringdragstart
What happens if there is an error in
"dragstart"
?
- Chrome: drag finished with
"dragend"
- Firefox: drag continues 👀
- Safari: drag finished with
"dragend"
The behaviour is the same as when you are in a drop target element. No additional drag events are fired; "dragend"
is all you get
What happens if there is an error in
"dragenter"
,"dragleave"
,"drag"
,"dragover"
?
- Chrome: drag continues
- Firefox: drag continues
- Safari: drag continues
What happens if there is an error outside of a drag event? eg in a
setTimeout
inside of"dragstart"
?
- Chrome: drag continues
- Firefox: drag continues
- Safari: drag continues
How do you know about a drag ending if the dragging "draggable"
element has been removed?
"dragend"
fires on the source element, so if that element is gone you won't get a"dragend"
- You will get a
"drop"
event if there was a successful drop on a drop target."drop"
comes just before"dragend"
and is dispatched on the drop target and not the"draggable"
element - You can listen for other input events (eg mouse, pointer or keyboard events) to see if the drag is finished. If you see any of the following events during what you think is a drag operation, then you know the drag has finished:
- If you see a
"dragstart"
event (a new drag is starting) - If you see a
"pointerdown"
event (these events happen before a"dragstart"
, so if you see one, then a drag is definitely finished - and maybe another one is about to start!) - If you see any user input events then you know that the last drag operation has finished
HTML Spec: From the moment that the user agent is to initiate the drag-and-drop operation, until the end of the drag-and-drop operation, device input events (e.g. mouse and keyboard events) must be suppressed.
- Sadly there is a bug in firefox where it lets through the first few user events during a drag (most I have seen is three). So what I do is listener for
"pointermove"
. If I see > 20 of those events, then I know the last drag finished by an error.react-dnd
waits 1s and then adds a"mousemove"
listener which is used for the same purpose. Might want to explore also counting keyboard events
- Sadly there is a bug in firefox where it lets through the first few user events during a drag (most I have seen is three). So what I do is listener for
- If you see a
You can use event.dataTransfer.items
to extract what entities are being dragged; most commonly useful for file dropping.
event.dataTransfer.items
is a DataTransferItemList
which is not an Array
. DataTransferItemList
does implement Symbol.iterator
so you can use it in loops, or convert it to an Array
.
- Chrome:
items
is populated (but items have identifying data stripped) - Safari:
items
is empty (this is correct asitems
should be disabled at this time) - Firefox:
items
is populated 👀
Do something like:
event.dataTransfer.items[0].getAsFile
- Chrome: you get
null
(makes sense,items
have had their information stripped) - Safari:
TypeError
(makes sense,items
is empty so we are doingundefined.getAsFile()
) - Firefox: you get
null
(respecting the security model)
live: the collection will be mutated in place as changes occur
To put this question more concretely:
Will
event.dataTransfer.items
from"dragstart"
become enabled (can you hold onto it?) or do you need to grab it again in"drop"
?
If items
is not live then you need to look the value up from the "drop"
event
Generally, items
is restricted so you can only access it properly in the "drop"
event.
- Chrome: not live
- Firefox: not live (appears live, but if you try to access an item in
"drop"
you won't get anything) - Safari: not live
- Chrome:
items.length
is the correct amount of files - Safari:
items.length
is0
! - Firefox:
items.length
is the correct amount of files
You cannot reliably know how many files are being dragged, so don't rely on .length
Do not use event.dataTransfer.items
in "dragstart"
or hold onto it for future use; collect .items
again in "drop"
You're my hero!