'dragleave' of parent element fires when dragging over children elements

Overview

I have the following HTML structure and I've attached the dragenter and dragleave events to the <div id="dropzone"> element.

<div id="dropzone">
    <div id="dropzone-content">
        <div id="drag-n-drop">
            <div class="text">this is some text</div>
            <div class="text">this is a container with text and images</div>
        </div>
    </div>
</div>

Problem

When I drag a file over the <div id="dropzone">, the dragenter event is fired as expected. However, when I move my mouse over a child element, such as <div id="drag-n-drop">, the dragenter event is fired for the <div id="drag-n-drop"> element and then the dragleave event is fired for the <div id="dropzone"> element.

If I hover over the <div id="dropzone"> element again, the dragenter event is again fired, which is cool, but then the dragleave event is fired for the child element just left, so the removeClass instruction is executed, which is not cool.

This behavior is problematic for 2 reasons:

  1. I'm only attaching dragenter & dragleave to the <div id="dropzone"> so I don't understand why the children elements have these events attached as well.

  2. I'm still dragging over the <div id="dropzone"> element while hovering over its children so I don't want dragleave to fire!

jsFiddle

Here's a jsFiddle to tinker with: http://jsfiddle.net/yYF3S/2/

Question

So... how can I make it such that when I'm dragging a file over the <div id="dropzone"> element, dragleave doesn't fire even if I'm dragging over any children elements... it should only fire when I leave the <div id="dropzone"> element... hovering/dragging around anywhere within the boundaries of the element should not trigger the dragleave event.

I need this to be cross-browser compatible, at least in the browsers that support HTML5 drag-n-drop, so this answer is not adequate.

It seems like Google and Dropbox have figured this out, but their source code is minified/complex so I haven't been able to figure this out from their implementation.


If you don't need to bind events to the child elements, you can always use the pointer-events property.

.child-elements {
  pointer-events: none;
}

I finally found a solution I'm happy with. I actually found several ways to do what I want but none were as successful as the current solution... in one solution, I experienced frequent flickering as a result of adding/removing a border to the #dropzone element... in another, the border was never removed if you hover away from the browser.

Anyway, my best hacky solution is this:

var dragging = 0;

attachEvent(window, 'dragenter', function(event) {

    dragging++;
    $(dropzone).addClass('drag-n-drop-hover');

    event.stopPropagation();
    event.preventDefault();
    return false;
});

attachEvent(window, 'dragover', function(event) {

    $(dropzone).addClass('drag-n-drop-hover');

    event.stopPropagation();
    event.preventDefault();
    return false;
});

attachEvent(window, 'dragleave', function(event) {

    dragging--;
    if (dragging === 0) {
        $(dropzone).removeClass('drag-n-drop-hover');
    }

    event.stopPropagation();
    event.preventDefault();
    return false;
});

This works pretty well but issues came up in Firefox because Firefox was double-invoking dragenter so my counter was off. But nevertheless, its not a very elegant solution.

Then I stumbled upon this question: How to detect the dragleave event in Firefox when dragging outside the window

So I took the answer and applied it to my situation:

$.fn.dndhover = function(options) {

    return this.each(function() {

        var self = $(this);
        var collection = $();

        self.on('dragenter', function(event) {
            if (collection.size() === 0) {
                self.trigger('dndHoverStart');
            }
            collection = collection.add(event.target);
        });

        self.on('dragleave', function(event) {
            /*
             * Firefox 3.6 fires the dragleave event on the previous element
             * before firing dragenter on the next one so we introduce a delay
             */
            setTimeout(function() {
                collection = collection.not(event.target);
                if (collection.size() === 0) {
                    self.trigger('dndHoverEnd');
                }
            }, 1);
        });
    });
};

$('#dropzone').dndhover().on({
    'dndHoverStart': function(event) {

        $('#dropzone').addClass('drag-n-drop-hover');

        event.stopPropagation();
        event.preventDefault();
        return false;
    },
    'dndHoverEnd': function(event) {

        $('#dropzone').removeClass('drag-n-drop-hover');

        event.stopPropagation();
        event.preventDefault();
        return false;
    }
});

This is clean and elegant and seems to be working in every browser I've tested so far (haven't tested IE yet).


This is a little ugly but it works dammit!...

On your 'dragenter' handler store the event.target (in a variable inside your closure, or whatever), then in your 'dragleave' handler only fire your code if event.target === the one you stored.

If your 'dragenter' is firing when you don't want it to (i.e. when it's entering after leaving child elements), then the last time it fires before the mouse leaves the parent, it's on the parent, so the parent will always be the final 'dragenter' before the intended 'dragleave'.

(function () {

    var droppable = $('#droppable'),
        lastenter;

    droppable.on("dragenter", function (event) {
        lastenter = event.target;
        droppable.addClass("drag-over");            
    });

    droppable.on("dragleave", function (event) {
        if (lastenter === event.target) {
            droppable.removeClass("drag-over");
        }
    });

}());

At first, I agreed with folks discarding the pointer-events: none approach. But then I asked myself:

Do you really need pointer-events to work on the child elements while dragging is in progress?

In my case, I have lots of stuff going on in the children, e.g. hover to show buttons for additional actions, inline-editing, etc... However, none of that is necessary or in fact even desired during a drag.

In my case, I use something like this to turn pointer events off selectively for all child nodes of the parent container:

  div.drag-target-parent-container.dragging-in-progress * {
    pointer-events: none;
  }

Use your favorite approach to add/remove the class dragging-in-progress in the dragEnter/dragLeave event handlers, as I did or do the same in dragStart, et. al.


This seems to be a Chrome bug.

The only workaround that I could think of was to create a transparent overlay element to capture your events: http://jsfiddle.net/yYF3S/10/

JS:

$(document).ready(function() {
    var dropzone = $('#overlay');

    dropzone.on('dragenter', function(event) {
        $('#dropzone-highlight').addClass('dnd-hover');
    });

    dropzone.on('dragleave', function(event) {
        $('#dropzone-highlight').removeClass('dnd-hover');
    });

});​

HTML:

<div id="dropzone-highlight">
    <div id="overlay"></div>

    <div id="dropzone" class="zone">
        <div id="drag-n-drop">
            <div class="text1">this is some text</div>
            <div class="text2">this is a container with text and images</div>
        </div>
    </div>
</div>

<h2 draggable="true">Drag me</h2>
​