The trick is to use a dropzone covering the entire page, and caching the target
of window.ondragenter
to compare with the target
of window.ondragleave
.
First, the dropzone:
<style>
div.dropzone
{
/* positions to point 0,0 - required for z-index */
position: fixed; top: 0; left: 0;
/* above all elements, even if z-index is used elsewhere
it can be lowered as needed, but this value surpasses
all elements when used on YouTube for example. */
z-index: 9999999999;
/* takes up 100% of page */
width: 100%; height: 100%;
/* dim the page with 50% black background when visible */
background-color: rgba(0,0,0,0.5);
/* a nice fade effect, visibility toggles after 175ms, opacity will animate for 175ms. note display:none cannot be animated. */
transition: visibility 175ms, opacity 175ms;
}
</style>
<!-- both visibility:hidden and display:none can be used,
but the former can be used in CSS animations -->
<div style="visibility:hidden; opacity:0" class="dropzone"></div>
Even though the dropzone will be covering the entire page, using visibility:hidden
or display:none
will hide it from view. I used visibility:hidden
so that CSS animations can be used to animate the transition.
Assigning the events
<script>
/* lastTarget is set first on dragenter, then
compared with during dragleave. */
var lastTarget = null;
window.addEventListener("dragenter", function(e)
{
lastTarget = e.target; // cache the last target here
// unhide our dropzone overlay
document.querySelector(".dropzone").style.visibility = "";
document.querySelector(".dropzone").style.opacity = 1;
});
window.addEventListener("dragleave", function(e)
{
// this is the magic part. when leaving the window,
// e.target happens to be exactly what we want: what we cached
// at the start, the dropzone we dragged into.
// so..if dragleave target matches our cache, we hide the dropzone.
// `e.target === document` is a workaround for Firefox 57
if(e.target === lastTarget || e.target === document)
{
document.querySelector(".dropzone").style.visibility = "hidden";
document.querySelector(".dropzone").style.opacity = 0;
}
});
</script>
So here's the process: You drag a file over the window, and window.ondragenter immediately fires. The target
is set to the root element, <html>
. Then you immediately unhide your dropzone, which covers the entire page. window.ondragenter
will fire again, this time the target being your dropzone. Each time the dragenter
event fires, it will cache the target, because this will be the target that will match the last window.ondragleave
event that fires when you drag out of the window.
Why does this work? I have no idea, but that is how to do it. This is pretty much the only working method that triggers when the user drags off the page.
I believe it works because once the dropzone is unhidden, it will always be the last target. It covers every pixel of the page, even the <html>
tag. This method relies on dragleave firing when leaving the window. Unfortunately there is a bug in Firefox that prevents it from working properly. Please vote for it so it'll get fixed sooner. As of Firefox 57.0.2, dragleave appears to fire properly. However, a workaround is required, checking document
instead of the cached element:
if(e.target === lastTarget || e.target === document)
Here's a JSBin of it in action. Tested working in latest Chrome, Firefox, Edge and IE11.