ChatGPT解决这个技术问题 Extra ChatGPT

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:

<div id="drag" draggable="true">drag me</div>

<hr>

<div id="drop">
    drop here
    <p>child</p>
    parent
</div>

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.

The bug pimvdb reported still exists in Webkit as of May 2012. I've countered it by also adding a class in dragover, which isn't anywhere close to nice since it fires so often, but appears to patch the issue a bit.
@ajm: Thanks, that works to an extent. However, on Chrome, there is a flash when entering or leaving the child element, presumably because dragleave is still fired in that case.
I have opened a jQuery UI bug upvotes are welcome so they can decide to put resources on it
@fguillen: I'm sorry but this has nothing to do with jQuery UI. In fact, jQuery isn't even needed to trigger the bug. I've filed a WebKit bug already but there is no update as of now.
@pimvdb, yep, I have seen the answer in my bug, which is the link to your WebKit bug?.. any how I can reproduce the same bug with FireFox :/

G
Greg Gum

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


OMG this is the most obvious solution and only had ONE vote... Come on people, you can do better. I was thinking about that but after seeing the level of sophistication of the first few answers I almost discarded it. Did you have any drawbacks ?
This didn't work when the edge of the element being dragged touches the edge of another draggable element. For instance a sortable list; dragging the element downwards, over the next draggable item does not decrement the counter back to 0, instead it gets stucked at 1. But if I drag it sideways, out of any other draggable element, it works. I went with the pointer-events: none on the children. When I start dragging I append a class that has this property, when drag's over I remove the class. Worked nicely on Safari and Chrome, but not on Firefox.
This worked for me in all browsers, but Firefox. I have a new solution which works everywhere in my case. On the first dragenter I save event.currentTarget in a new variable dragEnterTarget. As long as dragEnterTarget is set, I ignore further dragenter events, because they are from children. In all dragleave events I check dragEnterTarget === event.target. If this is false the event will be ignored as it was fired by a child. If this is true I reset dragEnterTarget to undefined.
Great solution. I needed to reset the counter to 0 in the function that handles accepting the drop though, otherwise subsequent drags didn't work as expected.
@Woody I didn't see his comment in the huge list of comments here, but yes, that's the fix. Why not incorporate that into your answer?
C
Community

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.

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.


The pointer-events property is the right solution going forward, but unfortunately it doesn't work in IE8-IE10, which are still widely used. Also, I should point out that your current jsFiddle doesn't even work in IE11, since it doesn't add the necessary event listeners and default behavior prevention.
Using pointer-events is indeed a good answer, I struggled a bit before finding out by myself, that answer should be higher.
What if your children is a Button? o.O
it works only if you do not have other controller elements (edit, delete) inside the area, because this solution blocks them too..
For interactive child elements inside the drop target, such as a button: Add pointer-events: none; to a class. Apply the class to the drop target, using ondragenter. Then remove the class from the drop target in ondragleave.
A
Ali Motevallian

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 el2.ondragenter is called and then el1.ondragleave. Only when the drag is leaving/entering the browser window, event.target will be '' in both el2.ondragenter and el1.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.


This is the best solution, it's better than the counter (especially if you delegate events) and it works with draggable children as well.
This doesn't help tho for the problem of duplicate dragenter events
Does this work for "children" that are css pseudo-elements or pseudo-classes? I couldn't get it to, but maybe I was doing it wrong.
As of March 2020 I've had the best luck with this solution as well. Note: some linters may complain about the use of == over ===. You are comparing the event target object references, so === works fine too.
Excellent solution - using React was as simple as tracking a new state to reference onDragEnter and check against in onDragLeave. This should be the accepted answer.
D
Diego T. Yamaguchi

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)


Not sure why, but it's not working consistently with Chrome. Sometimes leaving the area keeps the mask visible.
Actually, it's the border. Have the mask overlapping the border, with no border of its own, and it should work OK.
Note, your jsfiddle has a bug in it, in drag_drop, you should remove the hover class on "#box" not "#box-a"
This is a nice solution, but somehow it did not work for me. I finally figure out a workaround. For those of you who are looking for something else. you could try this: github.com/bingjie2680/jquery-draghover
This solutions works nice when the actual mask is the #drop::before or ::after. Also, beware that sometimes, when dragging fast, the "dragleave" fires before the "dragenter" has finished. If the dragenter add the class/pseudoelement, and the dragleave removes then, this can cause problem.
T
Theodore Brown

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.

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).


This answer shows a workaround for the undesirable firing but neglects the question "Is it possible to prevent dragleave from firing when dragging into a child element?" entirely.
Behaviour can get strange when dragging a file from outside the browser. In Firefox I've got "Entering child -> Entering Parent -> Leaving child -> Entering child -> Leaving child" without leaving the parent, which left with the "over" class. Old IE would need an attachEvent replacement for the addEventListener.
That solution depends strongly on the bubbling, the "false" in all addEventListener should be emphasized as essential (although that's the default behaviour), since many people may not know about that.
That single draggable adds an effect to the dropzone that doesn't appear when the dropzone is used for other draggable objects that doesn't trigger the dragstart event. Perhaps using everything as a dropzone for the effect of dragging while keeping the real dropzone with other handlers would do that.
@H.D. I updated my answer with info on using the pointer-events CSS property to prevent the dragleave event from firing in Chrome, Firefox, and IE11+. I also updated my other workaround to support IE8 and IE9, in addition to IE10. It is intentional that the dropzone effect is only added when dragging the "Drag me" link. Others can feel free to change this behavior as needed to support their use cases.
K
Kenneth Spencer

This fairly simple solution is working for me so far, assuming your event is attached to each drag element individually.

if (evt.currentTarget.contains(evt.relatedTarget)) {
  return;
}

This is great! thanks for sharing @kenneth, you saved my day! A lot of the other answers only apply if you have a single droppable zone.
For me, it works in Chrome and Firefox, but id doesn't work in Edge. Please see: jsfiddle.net/iwiss/t9pv24jo
b
broc.seib

if you are using HTML5, you can get the parent's clientRect:

let rect = document.getElementById("drag").getBoundingClientRect();

Then in the parent.dragleave():

dragleave(e) {
    if(e.clientY < rect.top || e.clientY >= rect.bottom || e.clientX < rect.left || e.clientX >= rect.right) {
        //real leave
    }
}

here is a jsfiddle


Excellent answer.
Thanks buddy, i like the plain javascript approach.
When using on elements having border-radius, moving the pointer close to the corner would actually leave the element, but this code would still think we are inside (we have left the element but we are still in the bounding rectangle). Then the dragleave event handler won't be called at all.
F
FruityFred

A very simple solution is to use the pointer-events CSS property. Just set its value to none upon dragstart on every child element. These elements won't trigger mouse-related events anymore, so they won't catch the mouse over them and thus won't trigger the dragleave on the parent.

Don't forget to set this property back to auto when finishing the drag ;)


A
Alexandre Annic

A simple solution is to add the css rule pointer-events: none to the child component to prevent the trigger of ondragleave. See example:

function enter(event) { document.querySelector('div').style.border = '1px dashed blue'; } function leave(event) { document.querySelector('div').style.border = ''; } div { border: 1px dashed silver; padding: 16px; margin: 8px; } article { border: 1px solid silver; padding: 8px; margin: 8px; } p { pointer-events: none; background: whitesmoke; }

drag me
drop here

child not triggering dragleave


I had this same problem and your solution works great. Tip for others, this was my case: if the child element you're trying to make ignore the drag event is the clone of the element being dragged (trying to achieve a visual preview), you can use this inside dragstart when creating the clone: dragged = event.target;clone = dragged.cloneNode();clone.style.pointerEvents = 'none';
Nice, simple solution. Folks may want to consider just creating a class, .no-pointer-events {pointer-events: none;} then add no-pointer-events to each child element. Done.
This should be the accepted answer, it's the most simple and there are no hacks involved. :D
but what if the child component is a link?
G
Greg

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');
               }
           }

Thanks! I can't get this to work in Chrome however. Could you provide a working fiddle of your hack?
I was thinking of doing it by checking the coords too. You did most of the work for me, thx :). I had to make some adjustments though: if (e.x >= (rect.left + rect.width) || e.x <= rect.left || e.y >= (rect.top + rect.height) || e.y <= rect.top)
It won't work in Chrome because it's event doesn't have e.x and e.y.
I like this solution. Didn't work in Firefox first. But if you replace e.x with e.clientX and e.y with e.clientY it works. Also works in Chrome.
Did not work in chrome for me, neither did what Chris or Daniel Stuts suggested
O
Owen M

Very simple solution:

parent.addEventListener('dragleave', function(evt) {
    if (!parent.contains(evt.relatedTarget)) {
        // Here it is only dragleave on the parent
    }
}

This doesn't seem to work in Safari 12.1: jsfiddle.net/6d0qc87m
r
robertc

You can fix it in Firefox with a little inspiration from the jQuery source code:

dragleave: function(e) {
    var related = e.relatedTarget,
        inside = false;

    if (related !== this) {

        if (related) {
            inside = jQuery.contains(this, related);
        }

        if (!inside) {

            $(this).removeClass('red');
        }
    }

}

Unfortunately it doesn't work in Chrome because relatedTarget appears not to exist on dragleave events, and I assume you're working in Chrome because your example did't work in Firefox. Here's a version with the above code implemented.


Thanks a lot, but indeed it's Chrome I'm trying to solve this problem in.
@pimvdb I see you've logged a bug, I'll just leave a reference to it here in case anyone else comes across this answer.
I did indeed, but I forgot to add a link to it here. Thanks for doing that.
The Chrome bug has been fixed in the mean time.
A
Aldekein

And here it goes, a solution for Chrome:

.bind('dragleave', function(event) {
                    var rect = this.getBoundingClientRect();
                    var getXY = function getCursorPosition(event) {
                        var x, y;

                        if (typeof event.clientX === 'undefined') {
                            // try touch screen
                            x = event.pageX + document.documentElement.scrollLeft;
                            y = event.pageY + document.documentElement.scrollTop;
                        } else {
                            x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
                            y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
                        }

                        return { x: x, y : y };
                    };

                    var e = getXY(event.originalEvent);

                    // Check the mouseEvent coordinates are outside of the rectangle
                    if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
                        console.log('Drag is really out of area!');
                    }
                })

Does it go into «if (typeof event.clientX === 'undefined')»?
Worked nicely but there could be another window over the browser, so getting the mouse location and comparing it to the rectangular screen area isn't enough.
I agree with @H.D. Also, this will cause problems when element has large border-radius as I explained in my comment on @azlar's answer above.
r
rasmusx

I was having the same issue and tried to use pk7s solution. It worked but it could be done a little bit better without any extra dom elements.

Basicly the idea is same - add an extra unvisible overlay over droppable area. Only lets do this without any extra dom elements. Here is the part were CSS pseudo-elements come to play.

Javascript

var dragOver = function (e) {
    e.preventDefault();
    this.classList.add('overlay');
};

var dragLeave = function (e) {
    this.classList.remove('overlay');
};


var dragDrop = function (e) {
    this.classList.remove('overlay');
    window.alert('Dropped');
};

var dropArea = document.getElementById('box');

dropArea.addEventListener('dragover', dragOver, false);
dropArea.addEventListener('dragleave', dragLeave, false);
dropArea.addEventListener('drop', dragDrop, false);

CSS

This after rule will create a fully covered overlay for droppable area.

#box.overlay:after {
    content:'';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    z-index: 1;
}

Here is the full solution: http://jsfiddle.net/F6GDq/8/

I hope it helps anyone with the same problem.


Occasionally does not work on chrome (does not catch dragleave properly)
#box must be position relative for this to work correctly; but I was able to get this solution working very cleanly. Thanks!
m
marcelj

Here's another solution using document.elementFromPoint:

 dragleave: function(event) {
   var event = event.originalEvent || event;
   var newElement = document.elementFromPoint(event.pageX, event.pageY);
   if (!this.contains(newElement)) {
     $(this).removeClass('red');
   }
}

Hope this works, here's a fiddle.


This didn't work for me out of the box. I needed to use event.clientX, event.clientY instead, since they're relative to the viewport and not the page. I hope that helps another lost soul out there.
c
chrisallick

Not sure if this cross browser, but I tested in Chrome and it solves my problem:

I want to drag and drop a file over entire page, but my dragleave is fired when i drag over child element. My fix was to look at the x and y of mouse:

i have a div that overlays my entire page, when the page loads i hide it.

when you drag over document i show it, and when you drop on the parent it handles it, and when you leave the parent i check x and y.

$('#draganddrop-wrapper').hide();

$(document).bind('dragenter', function(event) {
    $('#draganddrop-wrapper').fadeIn(500);
    return false;
});

$("#draganddrop-wrapper").bind('dragover', function(event) {
    return false;
}).bind('dragleave', function(event) {
    if( window.event.pageX == 0 || window.event.pageY == 0 ) {
        $(this).fadeOut(500);
        return false;
    }
}).bind('drop', function(event) {
    handleDrop(event);

    $(this).fadeOut(500);
    return false;
});

Hacky but clever. I like. Worked for me.
Oh, how I wish this answer had more votes! Thank you
M
Marcin Raczkowski

I've stumbled into the same problem and here's my solution - which I think is much easier then above. I'm not sure if it's crossbrowser (might depend on even bubbling order)

I'll use jQuery for simplicity, but solution should be framework independent.

The event bubbles to parent either way so given:

<div class="parent">Parent <span>Child</span></div>

We attach events

el = $('.parent')
setHover = function(){ el.addClass('hovered') }
onEnter  = function(){ setTimeout(setHover, 1) }
onLeave  = function(){ el.removeClass('hovered') } 
$('.parent').bind('dragenter', onEnter).bind('dragleave', onLeave)

And that's about it. :) it works because even though onEnter on child fires before onLeave on parent, we delay it slightly reversing the order, so class is removed first then reaplied after a milisecond.


Only thing this snippet does is preventing the 'hovered' class to be removed by reapplying it the next tick cycle (making the 'dragleave' event useless).
It's not useless. If you leave parent it'll work as expected. The power of this solution is it's simplicity, it's not ideal or best there is. Better solution would be to mark an enter on a child, in onleave check if we just entered child, and if so not trigger leave event. It'll hovever need testing, extra guards, checing for grandchildren, etc.
P
Profet

An alternate working solution, a little simpler.

//Note: Due to a bug with Chrome the 'dragleave' event is fired when hovering the dropzone, then
//      we must check the mouse coordinates to be sure that the event was fired only when 
//      leaving the window.
//Facts:
//  - [Firefox/IE] e.originalEvent.clientX < 0 when the mouse is outside the window
//  - [Firefox/IE] e.originalEvent.clientY < 0 when the mouse is outside the window
//  - [Chrome/Opera] e.originalEvent.clientX == 0 when the mouse is outside the window
//  - [Chrome/Opera] e.originalEvent.clientY == 0 when the mouse is outside the window
//  - [Opera(12.14)] e.originalEvent.clientX and e.originalEvent.clientY never get
//                   zeroed if the mouse leaves the windows too quickly.
if (e.originalEvent.clientX <= 0 || e.originalEvent.clientY <= 0) {

This doesn't appear to always work in Chrome. I would on occasion receive clientX above 0 when the mouse is outside of the box. Granted, my elements are position:absolute
Does it happens all the time or only sometimes ? Because if the mouse is moving too fast (ig. outside the window), you might get wrong values.
It happens 90% of the time. There are rare cases (1 out of 10 times) where I can make it reach 0. I'll try again moving the mouse slower, but I couldn't say I was moving quickly (perhaps what you'd call normal speed).
Using dropzone.js a similiar aproach worked for me: console.log(e.clientX + "/" + e.clientY ); if (e.clientX == 0 && e.clientY == 0 ) { console.log('REAL leave'); }
B
Ben

I've written a little library called Dragster to handle this exact issue, works everywhere except silently doing nothing in IE (which doesn't support DOM Event Constructors, but it'd be pretty easy to write something similar using jQuery's custom events)


Very useful (at least for me where I only care about Chrome).
s
sofarsoghood

Just check if the dragged over element is a child, if it is, then don't remove your 'dragover' style class. Pretty simple and works for me:

 $yourElement.on('dragleave dragend drop', function(e) {
      if(!$yourElement.has(e.target).length){
           $yourElement.removeClass('is-dragover');
      }
  })

Looks like the most simple solution to me, and it solved my problem. Thanks!
B
B T

I wrote a drag-and-drop module called drip-drop that fixes this weirdo behavior, among others. If you're looking for a good low-level drag-and-drop module you can use as the basis for anything (file upload, in-app drag-and-drop, dragging from or to external sources), you should check this module out:

https://github.com/fresheneesz/drip-drop

This is how you would do what you're trying to do in drip-drop:

$('#drop').each(function(node) {
  dripDrop.drop(node, {
    enter: function() {
      $(node).addClass('red')  
    },
    leave: function() {
      $(node).removeClass('red')
    }
  })
})
$('#drag').each(function(node) {
  dripDrop.drag(node, {
    start: function(setData) {
      setData("text", "test") // if you're gonna do text, just do 'text' so its compatible with IE's awful and restrictive API
      return "copy"
    },
    leave: function() {
      $(node).removeClass('red')
    }
  })
})

To do this without a library, the counter technique is what I used in drip-drop, tho the highest rated answer misses important steps that will cause things to break for everything except the first drop. Here's how to do it properly:

var counter = 0;    
$('#drop').bind({
    dragenter: function(ev) {
        ev.preventDefault()
        counter++
        if(counter === 1) {
          $(this).addClass('red')
        }
    },

    dragleave: function() {
        counter--
        if (counter === 0) { 
            $(this).removeClass('red');
        }
    },
    drop: function() {
        counter = 0 // reset because a dragleave won't happen in this case
    }
});

A
Abubakar Azeem

I found a simple solution to this problem so sharing it. It works well in my case.

jsfiddle try it.

You can actually achieve this only via the dragenter event and you don't even need to register a dragleave. All you need is to have a no-drop area around your dropzones and that's it.

You can also have nested dropzones and this works perfectly. Check this as well nested dropzones.

$('.dropzone').on("dragenter", function(e) {
  e.preventDefault();
  e.stopPropagation();
  $(this).addClass("over");
  $(".over").not(this).removeClass("over"); // in case of multiple dropzones
});

$('.dropzone-leave').on("dragenter", function(e) {
  e.preventDefault();
  e.stopPropagation();
  $(".over").removeClass("over");
});

// UPDATE
// As mar10 pointed out, the "Esc" key needs to be managed,
// the easiest approach is to detect the key and clean things up.

$(document).on('keyup', function(e){
  if (e.key === "Escape") {
    $(".over").removeClass("over");
  }
});

I tried this solution, but it should be noted it has a limitation: when pressing Esc to cancel the drag&drop action, no one will remove the "over" class from your dropzone element. If you are trying to use "dragleave" event for this, you just go to the first problem the original author asked about. So both hovering a child element and pressing Esc key while dragging will raise "dragleave" event on the dropzone. Maybe we also need to listen for the Esc key to remove the "over" class from the dropzone...
hi @mar10, thanks for pointing this issue, i will update my answer. i think we can use dragend event to handle this, but i will need to test that.
I haven't found a way to detect if the dragend event is triggered by releasing a mouse button or pressing the Esc key, so going with the dragend event will complicate the logic, so the simple solution is to detect the Escape key as you said. I've updated the answer.
B
Brad Larson

After spending so many hours I got that suggestion working exactly as intended. I wanted to provide a cue only when files were dragged over, and document dragover, dragleave was causing painful flickers on Chrome browser.

This is how I solved it, also throwing in proper cues for user.

$(document).on('dragstart dragenter dragover', function(event) {    
    // Only file drag-n-drops allowed, http://jsfiddle.net/guYWx/16/
    if ($.inArray('Files', event.originalEvent.dataTransfer.types) > -1) {
        // Needed to allow effectAllowed, dropEffect to take effect
        event.stopPropagation();
        // Needed to allow effectAllowed, dropEffect to take effect
        event.preventDefault();

        $('.dropzone').addClass('dropzone-hilight').show();     // Hilight the drop zone
        dropZoneVisible= true;

        // http://www.html5rocks.com/en/tutorials/dnd/basics/
        // http://api.jquery.com/category/events/event-object/
        event.originalEvent.dataTransfer.effectAllowed= 'none';
        event.originalEvent.dataTransfer.dropEffect= 'none';

         // .dropzone .message
        if($(event.target).hasClass('dropzone') || $(event.target).hasClass('message')) {
            event.originalEvent.dataTransfer.effectAllowed= 'copyMove';
            event.originalEvent.dataTransfer.dropEffect= 'move';
        } 
    }
}).on('drop dragleave dragend', function (event) {  
    dropZoneVisible= false;

    clearTimeout(dropZoneTimer);
    dropZoneTimer= setTimeout( function(){
        if( !dropZoneVisible ) {
            $('.dropzone').hide().removeClass('dropzone-hilight'); 
        }
    }, dropZoneHideDelay); // dropZoneHideDelay= 70, but anything above 50 is better
});

L
Lalit Nankani

"dragleave" event is fired when mouse pointer exits the dragging area of the target container.

Which makes a lot of sense as in many cases only the parent may be droppable and not the descendants. I think event.stopPropogation() should have handled this case but seems like it doesn't do the trick.

Above mentioned some solutions do seem to work for most of the cases, but fails in case of those children which does not support dragenter / dragleave events, such as iframe.

1 workaround is to check the event.relatedTarget and verify if it resides inside the container then ignore the dragleave event as I have done here:

function isAncestor(node, target) {
    if (node === target) return false;
    while(node.parentNode) {
        if (node.parentNode === target)
            return true;
        node=node.parentNode;
    }
    return false;
}

var container = document.getElementById("dropbox");
container.addEventListener("dragenter", function() {
    container.classList.add("dragging");
});

container.addEventListener("dragleave", function(e) {
    if (!isAncestor(e.relatedTarget, container))
        container.classList.remove("dragging");
});

You can find a working fiddle here!


C
Chris Kirby

I know this is a old question but wanted to add my preference. I deal with this by adding class triggered css :after element at a higher z-index then your content. This will filter out all the garbage.

.droppable{
    position: relative;
    z-index: 500;
}

.droppable.drag-over:after{
    content: "";
    display:block;
    position:absolute;
    left:0;
    right:0;
    top:0;
    bottom:0;
    z-index: 600;
}

Then just add the drag-over class on your first dragenter event and none of the child elements trigger the event any longer.

dragEnter(event){
 dropElement.classList.add('drag-over');
}

dragLeave(event){
 dropElement.classList.remove('drag-over');
}

A
Arseny

I had a similar problem — my code for hiding the dropzone on dragleave event for body was fired contatantly when hovering child elements making the dropzone flicker in Google Chrome.

I was able to solve this by scheduling the function for hiding dropzone instead of calling it right away. Then, if another dragover or dragleave is fired, the scheduled function call is cancelled.

body.addEventListener('dragover', function() {
    clearTimeout(body_dragleave_timeout);
    show_dropzone();
}, false);

body.addEventListener('dragleave', function() {
    clearTimeout(body_dragleave_timeout);
    body_dragleave_timeout = setTimeout(show_upload_form, 100);
}, false);

dropzone.addEventListener('dragover', function(event) {
    event.preventDefault();
    dropzone.addClass("hover");
}, false);

dropzone.addEventListener('dragleave', function(event) {
    dropzone.removeClass("hover");
}, false);

This is what I ended up doing too, but it's still sketchy.
P
Pratham

Solved ..!

Declare any array for ex:

targetCollection : any[] 

dragenter: function(e) {
    this.targetCollection.push(e.target); // For each dragEnter we are adding the target to targetCollection 
    $(this).addClass('red');
},

dragleave: function() {
    this.targetCollection.pop(); // For every dragLeave we will pop the previous target from targetCollection
    if(this.targetCollection.length == 0) // When the collection will get empty we will remove class red
    $(this).removeClass('red');
}

No need to worry about child elements.


D
DevelJoe

I struggeled a LOT with this, even after reading through all of these answers, and thought I may share my solution with you, because I figured it may be one of the simpler approaches, somewhat different though. My thought was of simply omitting the dragleave event listener completely, and coding the dragleave behaviour with each new dragenter event fired, while making sure that dragenter events won't be fired unnecessarily.

In my example below, I have a table, where I want to be able to exchange table row contents with each other via drag & drop API. On dragenter, a CSS class shall be added to the row element into which you're currently dragging your element, to highlight it, and on dragleave, this class shall be removed.

Example:

Very basic HTML table:

<table>
  <tr>
    <td draggable="true" class="table-cell">Hello</td>
  </tr>
  <tr>
    <td draggable="true" clas="table-cell">There</td>
  </tr>
</table>

And the dragenter event handler function, added onto each table cell (aside dragstart, dragover, drop, and dragend handlers, which are not specific to this question, so not copied here):

/*##############################################################################
##                              Dragenter Handler                             ##
##############################################################################*/

// When dragging over the text node of a table cell (the text in a table cell),
// while previously being over the table cell element, the dragleave event gets
// fired, which stops the highlighting of the currently dragged cell. To avoid
// this problem and any coding around to fight it, everything has been
// programmed with the dragenter event handler only; no more dragleave needed

// For the dragenter event, e.target corresponds to the element into which the
// drag enters. This fact has been used to program the code as follows:

var previousRow = null;

function handleDragEnter(e) {
  // Assure that dragenter code is only executed when entering an element (and
  // for example not when entering a text node)
  if (e.target.nodeType === 1) {
    // Get the currently entered row
    let currentRow = this.closest('tr');
    // Check if the currently entered row is different from the row entered via
    // the last drag
    if (previousRow !== null) {
      if (currentRow !== previousRow) {
        // If so, remove the class responsible for highlighting it via CSS from
        // it
        previousRow.className = "";
      }
    }
    // Each time an HTML element is entered, add the class responsible for
    // highlighting it via CSS onto its containing row (or onto itself, if row)
    currentRow.className = "ready-for-drop";
    // To know which row has been the last one entered when this function will
    // be called again, assign the previousRow variable of the global scope onto
    // the currentRow from this function run
    previousRow = currentRow;
  }
}

Very basic comments left in code, such that this code suits for beginners too. Hope this will help you out! Note that you will of course need to add all the event listeners I mentioned above onto each table cell for this to work.


s
smrtl

Here is another approach based on the timing of events.

The dragenter event dispatched from the child element can be captured by the parent element and it always occurs before the dragleave. The timing between these two events is really short, shorter than any possible human mouse action. So, the idea is to memorize the time when a dragenter happens and filter dragleave events that occurs "not too quickly" after ...

This short example works on Chrome and Firefox:

var node = document.getElementById('someNodeId'),
    on   = function(elem, evt, fn) { elem.addEventListener(evt, fn, false) },
    time = 0;

on(node, 'dragenter', function(e) {
    e.preventDefault();
    time = (new Date).getTime();
    // Drag start
})

on(node, 'dragleave', function(e) {
    e.preventDefault();
    if ((new Date).getTime() - time > 5) {
         // Drag end
    }
})

a
abhi

pimvdb..

Why don't you try out using drop instead of dragleave. It worked for me. hope this solves your problem.

Please check the jsFiddle : http://jsfiddle.net/HU6Mk/118/

$('#drop').bind({
                 dragenter: function() {
                     $(this).addClass('red');
                 },

                 drop: function() {
                     $(this).removeClass('red');
                 }
                });

$('#drag').bind({
                 dragstart: function(e) {
                     e.allowedEffect = "copy";
                     e.setData("text/plain", "test");
                 }
                });

If the user changes their mind and drags the file back out of the browser, this will remain red.