23
votes

While writing web apps that took file input, I wanted to use drag 'n' drop, but I didn't want just a small dropzone on the page. I thought it would be more convenient if you could drop anywhere on the page. Luckily, the window.ondrop event fires anywhere on the page, but I wanted some fancy effect to show the user visually that drag/drop was possible.

To do that, all that was needed was detect when a file was dragged into the window, and when it was dragged out, to trigger an effect that showed the user that the app was drag-enabled. Turns out that drag events are not that convenient. I assumed that window.ondragenter would trigger only once, when the user entered the page. Then when you left the window, it'd trigger window.ondragleave. Wrong. It's constantly firing as the mouse moves over child elements in the page.

I looked at what properties were available in the event object, trying to find anything that could isolate what I needed, but nothing worked. The furtherest I got was being able to change the background color of body. And only if there was nothing else on the page.

Tons of file upload sites got it right. Imgur and WeTransfer for example. Their sites were all spahetti-coded and compressed to the point of unreadability, and I couldn't find anything on the subject by googling.

So how can this be done?

2
I know it's 6 years later, but it appears to now be possible to use Javascript to bind a handler to drop and dragover on the root HTML element of the page. Is there any reason this is a bad idea?pbarranis
Ah, another question exists for doing this much more simply using HTML5 and the root html element as the drop target. Yes, this does work fine. stackoverflow.com/questions/3144881/…pbarranis
@pbarranis I don't get what you're asking. The question you linked is exactly the same problem, just worded differently. He's talking about inconsistent firing of dragenter/dragleave. My answer addresses the root issue, whereas the alternative answer takes the setTimeout approach that Gmail and Imgur used around 2013. I specifically tried to find a better, more responsive solution than setTimeout. My solution does use JavaScript to bind to drag events to the root element (window). It is not a bad idea, because I already do it.bryc
Well to be fair, that question is specifically "how do I highlight a small dropzone as soon as the user drags into the page". This one is "highlight the entire page as a dropzone as soon as the user drags into the page". But the underlying mechanic is similar.bryc

2 Answers

37
votes

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.

0
votes

I changed the accepted answer a little because didn't want to hide Dropzone. So maybe this tiny modification be helpful for others:

1- Made a full width hidden wrapper:

<div class="wrapper" style="visibility:hidden; opacity:0" >DROP HERE</div>

2- Show it on [dragenter] event:

window.addEventListener("dragenter", function(e){ // drag start
    // unhide our green overlay
    showWrapper();
    lastTarget = e.target; // cache the last target here
});

3- if user drop the file, pass it to Dropzone:

window.addEventListener("drop", function(e){

    e.preventDefault();
    hideWrapper();

    // if drop, we pass object file to dropzone
    var myDropzone = Dropzone.forElement(".dropzone");
    myDropzone.handleFiles(e.dataTransfer.files);

});

I made a live demo with more details on codepen. Thank you