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>