Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript - Double File Dragover Event Firing

I'm trying to create a file drag/drop handler (drag a file into the browser window, to be used for upload).

For some reason when I bind the drag/drop listener to $("body") instead of to a $("div") in the body the events fire several times in a row, sometimes even non-stop (seemingly looping). What could be causing this?

Here's a trimmed down version of the code: http://jsfiddle.net/WxMwK/9/

var over = false;

$("body")
    .on("dragover", function(e){
        e.preventDefault();
        if (! over) {
            over = true;
            $("ul").append($("<li/>").text("dragover"));    
        }
    })
    .on("dragleave", function(e){
        e.preventDefault();
        if (over) {
            over = false;
            $("ul").append($("<li/>").text("dragleave"));
        }
    })
    .on("drop", function(e){
        e.preventDefault();
        if (over) {
            over = false;
            $("ul").append($("<li/>").text("drop"));
        }
    }); 

To test: drag a file into the orange area, you'll see the event firing multiple times in a row.

like image 853
Ian Avatar asked Dec 11 '22 18:12

Ian


1 Answers

The anon is (mostly) correct. To put it simply: when the mouse moves over the edge of an element inside your drop target, you get a dropenter for the element under the cursor and a dropleave for the element that was under the cursor previously. This happens for absolutely any descendant.

You can't check the element associated with dragleave, because if you move the mouse from your drop target onto a child element, you'll get a dropenter for the child and then a dropleave for the target! It's kind of ridiculous and I don't see how this is a useful design at all.

Here's a crappy jQuery-based solution I came up with some time ago.

var $drop_target = $(document.body);
var within_enter = false;

$drop_target.bind('dragenter', function(evt) {
    // Default behavior is to deny a drop, so this will allow it
    evt.preventDefault();

    within_enter = true;
    setTimeout(function() { within_enter = false; }, 0);

    // This is the part that makes the drop area light up
    $(this).addClass('js-dropzone');
});
$drop_target.bind('dragover', function(evt) {
    // Same as above
    evt.preventDefault();
});
$drop_target.bind('dragleave', function(evt) {
    if (! within_enter) {
        // And this makes it un-light-up  :)
        $(this).removeClass('js-dropzone');
    }
    within_enter = false;
});

// Handle the actual drop effect
$drop_target.bind('drop', function(evt) {
    // Be sure to reset your state down here
    $(this).removeClass('js-dropzone');
    within_enter = false;

    evt.preventDefault();

    do_whatever(evt.originalEvent.dataTransfer.files);
});

The trick relies on two facts:

  • When you move the mouse from a grandchild into a child, both dragenter and dragleave will be queued up for the target element—in that order.
  • The dragenter and dragleave are queued together.

So here's what happens.

  • In the dragenter event, I set some shared variable to indicate that the drag movement hasn't finished resolving yet.
  • I use setTimeout with a delay of zero to immediately change that variable back.
  • But! Because the two events are queued at the exact same time, the browser won't run any scheduled functions until both events have finished resolving. So the next thing that happens is dragleave's event handler.
  • If dragleave sees that it was paired with a dragenter on the same target element, that means the mouse must have moved from some descendant to some other descendant. Otherwise, the mouse is actually leaving the target element.
  • Then the setTimeout finally resolves zero seconds later, setting back the variable before another event can come along.

I can't think of a simpler approach.

like image 91
Eevee Avatar answered Dec 14 '22 07:12

Eevee