I'd like to handle dragend
events differently depending on whether an element has been just dragged inside the browser window (or site resp.) or or outside, e.g. to an external file manager.
After I didn't find any attribute of the DragEvent
instance indicating whether it's inside or outside the sites context I started to arithmetically figure out if the corresponding mouse event still takes place inside the geometry of the site.
Eventually I might succeed with that approach (currently not working yet) but it has one major disadvantage (leaving alone its ugliness): the drop target window might be on top of the browser, so the geometry is no real indicator at all..
so.. how do I find out if a dragend
(or any other event I could use to store some state) is pointing outside of the browser window (or source site)?
I couldn't find any super straightforward ways to do this, but I could do it fairly concisely with a handful of listeners. If you're ok with having a variable to assist with the state then you can do something like this.
First, listen for the mouse leaving on a drag event. I've found the most reliable way to do this is to use a dragleave
listener and then examine the event data to make sure it's really leaving the window. This event runs a ton though, so we need to filter out the ones we need.
dragleave
runs every time any element's drop zone is left. To make sure that the drag event is just leaving the page, we can check the target to make sure leaving the html
or body
tag and going to null
. This explains how to see the correct targets for the event.
Right before the dragend
event, dragleave
is ran as though it left the window. This is problematic because it makes every drop seem as though it were out of the window. It seems that this behavior isn't well defined in the specs and there is some variation between how Firefox and Chrome handle this.
For Chrome, we can make the code in dragleave
run a cycle after the code in dragend
by wrapping it in a timeout with 0 seconds.
This doesn't work well in Firefox though because the dragend
event doesn't come as fast. Firefox does, however, set buttons
to 0 so we know it's the end of an event.
Here is what the dragleave
event listener might look like
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
From here we just need to do something similar to see when the drag re-enters the viewport. This won't run before dragend
like dragleave
so it is simpler.
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
It's also a good idea to reset outside
every time the drag event starts.
element.addEventListener('dragstart', (e) => {
outside = false;
});
Now it's possible in the dragend
event listener to see where the drop ended.
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
Here is a snippet with everything put together (or fiddle)
Note #1: You'll need to drag the element out of the browser window, not just the demo window for it to appear as "outside".
Note #2: There has to be a better way for stopping the last dragleave
event, but after a few hours of trying other things, this seemed the most consistent and reliable.
const element = document.querySelector('div');
var outside = false;
element.addEventListener('dragend', (e) => {
console.log('Ended ' + (outside ? 'Outside' : 'Inside'));
});
element.addEventListener('dragstart', (e) => {
outside = false;
});
window.addEventListener('dragleave', (e) => {
window.setTimeout(() => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null && e.buttons != 0) {
outside = true;
}
});
});
window.addEventListener('dragenter', (e) => {
if ((e.target === document.documentElement || e.target === document.body) && e.relatedTarget == null) {
outside = false;
}
});
div[draggable] {
width: fit-content;
margin-bottom: 32px;
padding: 16px 32px;
background-color: black;
color: white;
}
<div draggable="true">Drag Me</div>