4

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.

5
  • Dont understand your example, but may be you should use .stopPropagation() to stop the event buble. It is sometimes the reason for multiple event treatment.. Commented Jan 18, 2013 at 4:22
  • 1
    api.jquery.com/event.preventDefault preventDefault is jquery's way of doing .stopPropagation(). Commented Jan 18, 2013 at 4:23
  • 1
    preventDefault is preventing the default behaviour of the event (if you are clicking on a link, preventDefault avoid to follow the link). Stop propagation stop the event bubling. There are 2 different thing. If you search the word "propagation" in the api page you wrote, you will not find anything.. Commented Jan 18, 2013 at 4:31
  • Nothing is supposed to happen (in this script), just handle the in/out of the drag action. When the drag is over the body I want to dim the screen and show a message, and hide it when they drop the file or leave the screen. Commented Jan 18, 2013 at 4:33
  • I believe you're correct about the bubbling though. I'm going to try that. Commented Jan 18, 2013 at 4:36

4 Answers 4

13

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.

Sign up to request clarification or add additional context in comments.

Comments

0

You are adding a listener on the BODY HTMLElement for the dragover, dragleave and drop.

When you continue to drag over the DIV, there is a dragleave that is fired because the mouse is no more dragging over the BODY, but over the DIV.

Secondly, as you are not stopping the bubble event on the DIV (no listener is set), the dragover fired on the DIV is bubling to the BODY.

If I resume:

The mouse enter the body (in dragover)

--> fire drag over (body)

The mouse enter the DIV in the body

--> fire drag leave (of BODY)

--> fire drag over (of DIV) --> event bubling --> fire drag over (of BODY)

There is a similar problem with mouseover and mouseout, which is fixed by using mouseenter and mouseleave.

May be you can try the same code using dragenter event type. If its not working, you can check if the event.target is the BODY. This test could help to skip undesired drag event.

Good luck

1 Comment

you've got it backwards; mouseenter and mouseleave are the events broken in this same manner, and mouseover + mouseout are the ones that work as you might expect. alas, there is no dragout event.
0
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;
    }
}); 

Comments

0

Or you could just use stop(); to stop animation buildup

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.