元素。问题当我将文件拖到
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:
I'm only attaching dragenter & dragleave to the
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.
e.stopPropagation();
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).
stopPropagation()
and preventDefault()
is preferred. I'd drop the return false
.
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.
dragstart
event and remove it in dragend
.
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>
The problem is, that your elements inside the dropzones are of course part of the dropzone and when you enter the children, you leave the parent. Solving this is not easy. You might try adding events to the children too adding your class again to the parent.
$("#dropzone,#dropzone *").on('dragenter', function(event) {
// add a class to #dropzone
event.stopPropagation(); // might not be necessary
event.preventDefault();
return false;
});
Your events will still fire multiple times but nobody will see.
//Edit: Use the dragmove event to permanently overwrite the dragleave event:
$("#dropzone,#dropzone *").on('dragenter dragover', function(event) {
// add a class to #dropzone
event.stopPropagation(); // might not be necessary
event.preventDefault();
return false;
});
Define the dragleave event only for the dropzone.
dragleave
... when I leave a child, I might still be within the parent #dropzone
but that won't fire the dragenter
event again :(
dragleave
event is fired for the child element just left, so again the removeClass
instruction is executed
dragleave
only for the #dropzone
... if I could, I wouldn't have this problem.
As benr mentioned in this answer, you can prevent child nodes to fire on events, but if you need to bind some events, do this:
#dropzone.dragover *{
pointer-events: none;
}
And add this one to your JS code:
$("#dropzone").on("dragover", function (event) {
$("#dropzone").addClass("dragover");
});
$("#dropzone").on("dragleave", function (event) {
$("#dropzone").removeClass("dragover");
});
If you're using jQuery, check this out: https://github.com/dancork/jquery.event.dragout
It's truly awesome.
Special event created to handle true dragleave functionality. HTML5 dragleave event works more like mouseout. This plugin was created to replicate the mouseleave style functionality whilst dragging. Usage Example: $('#myelement').on('dragout',function(event){ // YOUR CODE });
EDIT: actually, I don't think it's dependent on jQuery, you can probably just use the code even without it.
dragleave
even though I haven't left the parent dropzone.
dragout
was the only solution that worked for me.
My two cents: Hide a layer over your dropzone then show it when you dragenter, and target the dragleave on it.
Demo : https://jsfiddle.net/t6q4shat/
HTML
<div class="drop-zone">
<h2 class="drop-here">Drop here</h2>
<h2 class="drop-now">Drop now!</h2>
<p>Or <a href="#">browse a file</a></p>
<div class="drop-layer"></div>
</div>
CSS
.drop-zone{
padding:50px;
border:2px dashed #999;
text-align:center;
position:relative;
}
.drop-layer{
display:none;
position:absolute;
top:0;
left:0;
bottom:0;
right:0;
z-index:5;
}
.drop-now{
display:none;
}
JS
$('.drop-zone').on('dragenter', function(e){
$('.drop-here').css('display','none');
$('.drop-now').css('display','block');
$(this).find('.drop-layer').css('display','block');
return false;
});
$('.drop-layer').on('dragleave', function(e){
$('.drop-here').css('display','block');
$('.drop-now').css('display','none');
$(this).css('display','none');
return false;
});
@hristo I have a much more elegant solution. Check that if that is something you could use.
Your effort was not wasted after all. I managed to use yours at first, but had different problems in FF, Chrome. After spending so many hours I got that suggestion working exactly as intended.
Here's how the implementation is. I also took advantage of visual cues to correctly guide users about the drop zone.
$(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
});
var dropZoneHideDelay=70, dropZoneVisible=true;
dropZoneHideDelay, dropZoneVisible
are needed across two 'on'
document events in my implementation.
This answer can be found here:
HTML5 dragleave fired when hovering a child element
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');
}
}
});
My version:
$(".dropzone").bind("dragover", function(e){
console.log('dragover');
});
$(".dropzone").bind("dragleave", function(e) {
var stopDrag = false;
if (!e.relatedTarget) stopDrag = true;
else {
var parentDrop = $(e.relatedTarget).parents('.dropzone');
if (e.relatedTarget != this && !parentDrop.length) stopDrag = true;
}
if (stopDrag) {
console.log('dragleave');
}
});
With this layout:
<div class="dropzone">
<div class="inner-zone">Inner-zone</div>
</div>
I've made a dump of element classes for e.target
, e.currentTarget
, e.relatedTarget
for both dragover
and dragleave
events.
It showed me that on leaving the parent block (.dropzone
) e.relatedTarget
is not a child of this block so I know I'm out of the dropzone.
!e.relatedTarget
worked perfectly for me. Thank you @mortalis
Sorry it's javascript not jquery, but for me it's the most logical way to solve that. The browser should call dropleave (of the previous element) before dropenter (ofr the new element, as something can't enter something else before having leaved the forst thing, I don't understand why they did that ! So you just need to delay de dropleave like that:
function mydropleave(e)
{
e.preventDefault();
e.stopPropagation();
setTimeout(function(e){ //the things you want to do },1);
}
And the dropenter will happen after the dropleave, that's all !
So for me the approach pointer-events: none;
didn't work too well... So here is my alternative solution:
#dropzone {
position: relative;
}
#dropzone(.active)::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '';
}
This way it is not possible to dragleave
the parent (on a child) or dragover
a child element. hope this helps :)
*The '.active'-class I add when I dragenter
or dragleave
. But if you're working without that just leave the class away.
I did not feel satisfied with any of the workarounds presented here, because I do not want to lose control over the children elements.
So I've used a different logic approach, translating it into a jQuery plugin, called jquery-draghandler. It does absolutely not manipulate the DOM, guaranteeing high performances. Its usage is simple:
$(document).ready(function() {
$(selector).draghandler({
onDragEnter: function() {
// $(this).doSomething();
},
onDragLeave: function() {
// $(this).doSomethingElse();
}
});
});
It deals flawlessly with the problem without compromising any DOM functionality.
Download, details and explanations on its Git repository.
frame
s and your element is at the border of it. In that case, I do not suggest this approach.
Super simple quick fix for this, not tested extensively but works in Chrome right now.
Excuse the Coffeescript.
dragEndTimer = no
document.addEventListener 'dragover', (event) ->
clearTimeout dragEndTimer
$('body').addClass 'dragging'
event.preventDefault()
return no
document.addEventListener 'dragenter', (event) ->
$('section').scrollTop(0)
clearTimeout dragEndTimer
$('body').addClass 'dragging'
document.addEventListener 'dragleave', (event) ->
dragEndTimer = setTimeout ->
$('body').removeClass 'dragging'
, 50
This fixes the Chrome flicker bug, or at least the permutation of it that was causing me issues.
I'm actually liking what I see at https://github.com/lolmaus/jquery.dragbetter/ but wanted to share a possible alternative. My general strategy was to apply a background style to the dropzone (not its children) when dragentering it or any child of it (via bubbling). Then I remove the style when dragleaving the drop zone. Idea was when moving to a child, even if I remove the style from the dropzone when leaving it (the dragleave fires), I would simply reapply the style to the parent dropzone on dragentering any child. Problem is of course that when moving from the dropzone to a child of the dropzone, the dragenter gets fired on the child before the dragleave, so my styles got applied out of order. Solution for me was to use a timer to force the dragenter event back to the message queue, allowing me to process it AFTER the dragleave. I used a closure to access the event on the timer callback.
$('.dropzone').on('dragenter', function(event) {
(function (event) {
setTimeout(function () {
$(event.target).closest('.dropzone').addClass('highlight');
}, 0);
}) (event.originalEvent);
});
This seems to work in chrome, ie, firefox and works regardless of the number of children in the dropzone. I'm slightly uneasy about the timeout guaranteeing the resequencing of events, but it seems to work pretty well for my use case.
Here, one of the simplest solution ●︿●
Take a look at this fiddle <-- try dragging some file inside the box
You can do something like this:
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);
By that you should already know whats happening here. Just take a look at the fiddle, you know.
I've had a similar problem and I fixed it this way:
The problem: The function drop(ev) is fired when the user drops an element in the "drop zone" (ul element), but, unfortunately, also when the element is dropped in one of its children ( li elements).
The fix:
function drop(ev) {
ev.preventDefault();
data=ev.dataTransfer.getData('Text');
if(ev.target=="[object HTMLLIElement]")
{ev.target.parentNode.appendChild(document.getElementById(data));}
else{ev.target.appendChild(document.getElementById(data));}
}
I was trying to implement this myself for a file upload box where the box color would change when users dragged a file into the space.
I found a solution that is a nice mix of Javascript and CSS. Say you have a droppable zone with div
with id #drop
. Add this to your Javascript:
$('#drop').on('dragenter', function() {
$(this).addClass('dragover');
$(this).children().addClass('inactive');
});
$('#drop').on('dragleave', function() {
$(this).removeClass('dragover');
$(this).children().removeClass('inactive');
});
Then, add this to your CSS, to inactivate all children will class .inactive
:
#drop *.inactive {
pointer-events: none;
}
Thus, the children elements will be inactive for as long as the user is dragging the element over the box.
I was trying to implement this myself, and I did not want Jquery or any plugin either.
I wanted to handle the file upload, following way deemed best for me:
Files Structure:
--- /uploads {uploads directory}
--- /js/slyupload.js {javascript file.}
--- index.php
--- upload.php
--- styles.css {just a little styling..}
HTML Code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>my Dzone Upload...</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="uploads"></div>
<div class="dropzone" id="dropzone">Drop files here to upload</div>
<script type="text/javascript" src="js/slyupload.js"></script>
</body>
</html>
Then This is the attached Javascript File:: 'js/slyupload.js'
<!-- begin snippet: -->
<!-- language: lang-js -->
// JavaScript Document
//ondragover, ondragleave...
(function(){
var dropzone = document.getElementById('dropzone');
var intv;
function displayUploads(data){
//console.log(data, data.length);
var uploads = document.getElementById('uploads'),anchor,x;
for(x=0; x < data.length; x++){
//alert(data[x].file);
anchor = document.createElement('a');
anchor.href = data[x].file;
anchor.innerText = data[x].name;
uploads.appendChild(anchor);
}
}
function upload(files){
//console.log(files);
var formData = new FormData(),
xhr = new XMLHttpRequest(), //for ajax calls...
x; //for the loop..
for(x=0;x<files.length; x++){
formData.append('file[]', files[x]);
/*//do this for every file...
xhr = new XMLHttpRequest();
//open... and send individually..
xhr.open('post', 'upload.php');
xhr.send(formData);*/
}
xhr.onload = function(){
var data = JSON.parse(this.responseText); //whatever comes from our php..
//console.log(data);
displayUploads(data);
//clear the interval when upload completes...
clearInterval(intv);
}
xhr.onerror = function(){
console.log(xhr.status);
}
//use this to send all together.. and disable the xhr sending above...
//open... and send individually..
intv = setInterval(updateProgress, 50);
xhr.open('post', 'upload.php');
xhr.send(formData);
//update progress...
/* */
}
function updateProgress(){
console.log('hello');
}
dropzone.ondrop = function(e){
e.preventDefault(); //prevent the default behaviour.. of displaying images when dropped...
this.className = 'dropzone';
//we can now call the uploading...
upload(e.dataTransfer.files); //the event has a data transfer object...
}
dropzone.ondragover = function(){
//console.log('hello');
this.className = 'dropzone dragover';
return false;
}
dropzone.ondragleave = function(){
this.className = 'dropzone';
return false;
}
}(window));
CSS:
body{ font-family:Arial, Helvetica, sans-serif; font-size:12px; } .dropzone{ width:300px; height:300px; border:2px dashed #ccc; color:#ccc; line-height:300px; text-align:center; } .dropzone.dragover{ border-color:#000; color:#000; }
Use the greedy : true
to the child as a function of droppable
. Then start only event clicked on the first layer.
Success story sharing
:hover
styles or click event handlers). In case you want to preserve those events, here's another workaround I've been using: bensmithett.github.io/dragster.dropzone * {pointer-events: none;}
$('.element').addClass('dragging-over')
to the element you're dragging over, and$('.element').removeClass('dragging-over')
when you drag out. Then in your CSS you can have.element.dragging-over * { pointer-events: none; }
. That removes pointer events from all child elements only when you're dragging over.