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>