HTML5 dragleave fired when hovering a child element

HTML5 dragleave fired when hovering a child element

The problem I’m having is that the dragleave event of an element is fired when hovering a child element of that element. Also, dragenter is not fired when hovering back the parent element again.
I made a simplified fiddle: http://jsfiddle.net/pimvdb/HU6Mk/1/.
HTML:

drag me

drop here

child

parent

with the following JavaScript:
$(‘#drop’).bind({
dragenter: function() {
$(this).addClass(‘red’);
},

dragleave: function() {
$(this).removeClass(‘red’);
}
});

$(‘#drag’).bind({
dragstart: function(e) {
e.allowedEffect = “copy”;
e.setData(“text/plain”, “test”);
}
});

What it is supposed to do is notifying the user by making the drop div red when dragging something there. This works, but if you drag into the p child, the dragleave is fired and the div isn’t red anymore. Moving back to the drop div also doesn’t make it red again. It’s necessary to move completely out of the drop div and drag back into it again to make it red.
Is it possible to prevent dragleave from firing when dragging into a child element?
2017 Update: TL;DR, Look up CSS pointer-events: none; as described in @H.D.’s answer below that works in modern browsers and IE11.

Solutions/Answers:

Solution 1:

You just need to keep a reference counter, increment it when you get a dragenter, decrement when you get a dragleave. When the counter is at 0 – remove the class.

var counter = 0;

$('#drop').bind({
    dragenter: function(ev) {
        ev.preventDefault(); // needed for IE
        counter++;
        $(this).addClass('red');
    },

    dragleave: function() {
        counter--;
        if (counter === 0) { 
            $(this).removeClass('red');
        }
    }
});

Note: In the drop event, reset counter to zero, and clear the added class.

You can run it here

Solution 2:

Is it possible to prevent dragleave from firing when dragging into a child element?

Yes.

#drop * {pointer-events: none;}

That CSS seem to be enough for Chrome.

Related:  What packages are available for node.js to do image cropping? [closed]

While using it with Firefox, the #drop shouldn’t have text nodes directly (else there’s a strange issue where a element “leave it to itself”), so I suggest to leave it with only one element (e.g., use a div inside #drop to put everything inside)

Here’s a jsfiddle solving the original question (broken) example.

I’ve also made a simplified version forked from the @Theodore Brown example, but based only in this CSS.

Not all browsers have this CSS implemented, though:
http://caniuse.com/pointer-events

Seeing the Facebook source code I could find this pointer-events: none; several times, however it’s probably used together with graceful degradation fallbacks. At least it’s so simple and solves the problem for a lot of environments.

Solution 3:

Here, the simplest Cross-Browser solution (seriously):

jsfiddle <– try dragging some file inside the box

You can do something like that:

var dropZone= document.getElementById('box');
var dropMask = document.getElementById('drop-mask');

dropZone.addEventListener('dragover', drag_over, false);
dropMask.addEventListener('dragleave', drag_leave, false);
dropMask.addEventListener('drop', drag_drop, false);

In a few words, you create a “mask” inside the dropzone, with width & height inherited, position absolute, that will just show when the dragover starts.
So, after showing that mask, you can do the trick by attaching the others dragleave & drop events on it.

After leaving or dropping, you just hide the mask again.
Simple and without complications.

(Obs.: Greg Pettit advice — You must be sure that the mask hover the entire box, including the border)

Solution 4:

The “right” way to solve this issue is to disable pointer events on child elements of the drop target (as in @H.D.’s answer). Here’s a jsFiddle I created which demonstrates this technique. Unfortunately, this doesn’t work in versions of Internet Explorer prior to IE11, since they didn’t support pointer events.

Related:  How do I call a dynamically-named method in Javascript?

Luckily, I was able to come up with a workaround which does work in old versions of IE. Basically, it involves identifying and ignoring dragleave events which occur when dragging over child elements. Because the dragenter event is fired on child nodes before the dragleave event on the parent, separate event listeners can be added to each child node which add or remove an “ignore-drag-leave” class from the drop target. Then the drop target’s dragleave event listener can simply ignore calls which occur when this class exists. Here’s a jsFiddle demonstrating this workaround. It is tested and working in Chrome, Firefox, and IE8+.

Update:

I created a jsFiddle demonstrating a combined solution using feature detection, where pointer events are used if supported (currently Chrome, Firefox, and IE11), and the browser falls back to adding events to child nodes if pointer event support isn’t available (IE8-10).

Solution 5:

It has been quite some time after this question is asked and a lot of solutions (including ugly hacks) are provided.

I managed to fix the same problem I had recently thanks to the answer in this answer and thought it may be helpful to someone who comes through to this page.
The whole idea is to store the evenet.target in ondrageenter everytime it is called on any of the parent or child elements. Then in ondragleave check if the current target (event.target) is equal to the object you stored in ondragenter.

The only case these two are matched is when your drag is leaving the browser window.

The reason that this works fine is when the mouse leaves an element (say el1) and enters another element (say el2), first the el1.ondragenter is called and then el2.ondragleave. Only when the drag is leaving/entering the browser window, event.target will be '' in both el1.ondragenter and el2.ondragleave.

Here is my working sample. I have tested it on IE9+, Chrome, Firefox and Safari.

(function() {
    var bodyEl = document.body;
    var flupDiv = document.getElementById('file-drop-area');

    flupDiv.onclick = function(event){
        console.log('HEy! some one clicked me!');
    };

    var enterTarget = null;

    document.ondragenter = function(event) {
        console.log('on drag enter: ' + event.target.id);
        enterTarget = event.target;
        event.stopPropagation();
        event.preventDefault();
        flupDiv.className = 'flup-drag-on-top';
        return false;
    };

    document.ondragleave = function(event) {
        console.log('on drag leave: currentTarget: ' + event.target.id + ', old target: ' + enterTarget.id);
        //Only if the two target are equal it means the drag has left the window
        if (enterTarget == event.target){
            event.stopPropagation();
            event.preventDefault();
            flupDiv.className = 'flup-no-drag';         
        }
    };
    document.ondrop = function(event) {
        console.log('on drop: ' + event.target.id);
        event.stopPropagation();
        event.preventDefault();
        flupDiv.className = 'flup-no-drag';
        return false;
    };
})();

And here is a simple html page:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Multiple File Uploader</title>
<link rel="stylesheet" href="my.css" />
</head>
<body id="bodyDiv">
    <div id="cntnr" class="flup-container">
        <div id="file-drop-area" class="flup-no-drag">blah blah</div>
    </div>
    <script src="my.js"></script>
</body>
</html>

With proper styling what I have done is to make the inner div (#file-drop-area) much bigger whenever a file is dragged into the screen so that the user can easily drop the files into the proper place.

Related:  Programmatically open new pages on Tabs

Solution 6:

The problem is that the dragleave event is being fired when the mouse goes in front of the child element.

I’ve tried various methods of checking to see if the e.target element is the same as the this element, but couldn’t get any improvement.

The way I fixed this problem was a bit of a hack, but works 100%.

dragleave: function(e) {
               // Get the location on screen of the element.
               var rect = this.getBoundingClientRect();

               // Check the mouseEvent coordinates are outside of the rectangle
               if(e.x > rect.left + rect.width || e.x < rect.left
               || e.y > rect.top + rect.height || e.y < rect.top) {
                   $(this).removeClass('red');
               }
           }