clicktag - ¿Cómo detecto un evento de arrastre HTML5 que entra y sale de la ventana, como hace Gmail?
clicktag google web designer (9)
¡La respuesta de @Tyler es la mejor! Lo he votado. Después de pasar tantas horas conseguí esa sugerencia trabajando exactamente como estaba previsto.
$(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
});
Me gustaría poder resaltar el área de colocación tan pronto como el cursor que lleva un archivo entre en la ventana del navegador, exactamente de la forma en que lo hace Gmail. Pero no puedo hacer que funcione, y siento que me estoy perdiendo algo realmente obvio.
Sigo tratando de hacer algo como esto:
this.body = $(''body'').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
Pero eso dispara los eventos cada vez que el cursor se mueve hacia arriba y hacia afuera de otros elementos distintos del CUERPO, lo que tiene sentido, pero no funciona. Podría colocar un elemento encima de todo, cubrir toda la ventana y detectar eso, pero sería una manera horrible de hacerlo.
¿Qué me estoy perdiendo?
¿Añadir los eventos al document
parece funcionar? Probado con Chrome, Firefox, IE 10.
El primer elemento que obtiene el evento es <html>
, que debería estar bien, creo.
var dragCount = 0,
dropzone = document.getElementById(''dropzone'');
function dragenterDragleave(e) {
e.preventDefault();
dragCount += (e.type === "dragenter" ? 1 : -1);
if (dragCount === 1) {
dropzone.classList.add(''drag-highlight'');
} else if (dragCount === 0) {
dropzone.classList.remove(''drag-highlight'');
}
};
document.addEventListener("dragenter", dragenterDragleave);
document.addEventListener("dragleave", dragenterDragleave);
¿Ha notado que hay un retraso antes de que desaparezca la zona de descarga en Gmail? Mi conjetura es que lo tienen desaparecer en un temporizador (~ 500ms) que se reinicia por medio de un dragón o algún evento similar.
El núcleo del problema que describiste es que la hoja de arrastre se desencadena incluso cuando arrastras a un elemento secundario. Estoy tratando de encontrar una manera de detectar esto, pero todavía no tengo una solución limpia y elegante.
Cuando el archivo ingresa y deja elementos secundarios, dispara dragenter
y dragleave
por lo que debe contar hacia arriba y hacia abajo.
var count = 0
document.addEventListener("dragenter", function() {
if (count === 0) {
setActive()
}
count++
})
document.addEventListener("dragleave", function() {
count--
if (count === 0) {
setInactive()
}
})
document.addEventListener("drop", function() {
if (count > 0) {
setInactive()
}
count = 0
})
Lamento mucho publicar algo que sea específico para el angulo y el subrayado, sin embargo, la forma en que resolví el problema (especificación de HTML5, funciona en Chrome) debería ser fácil de observar.
.directive(''documentDragAndDropTrigger'', function(){
return{
controller: function($scope, $document){
$scope.drag_and_drop = {};
function set_document_drag_state(state){
$scope.$apply(function(){
if(state){
$document.context.body.classList.add("drag-over");
$scope.drag_and_drop.external_dragging = true;
}
else{
$document.context.body.classList.remove("drag-over");
$scope.drag_and_drop.external_dragging = false;
}
});
}
var drag_enters = [];
function reset_drag(){
drag_enters = [];
set_document_drag_state(false);
}
function drag_enters_push(event){
var element = event.target;
drag_enters.push(element);
set_document_drag_state(true);
}
function drag_leaves_push(event){
var element = event.target;
var position_in_drag_enter = _.find(drag_enters, _.partial(_.isEqual, element));
if(!_.isUndefined(position_in_drag_enter)){
drag_enters.splice(position_in_drag_enter,1);
}
if(_.isEmpty(drag_enters)){
set_document_drag_state(false);
}
}
$document.bind("dragenter",function(event){
console.log("enter", "doc","drag", event);
drag_enters_push(event);
});
$document.bind("dragleave",function(event){
console.log("leave", "doc", "drag", event);
drag_leaves_push(event);
console.log(drag_enters.length);
});
$document.bind("drop",function(event){
reset_drag();
console.log("drop","doc", "drag",event);
});
}
};
})
Uso una lista para representar los elementos que han activado un evento de ingreso de arrastre. cuando ocurre un evento de permiso de arrastre, encuentro el elemento en la lista de ingreso de arrastre que coincide, lo quito de la lista, y si la lista resultante está vacía, sé que he arrastrado fuera del documento / ventana.
Necesito restablecer la lista que contiene elementos arrastrados sobre los elementos después de que ocurra un evento de colocación, o la próxima vez que comience a arrastrar algo, la lista se completará con elementos de la última acción de arrastrar y soltar.
Sólo he probado esto en Chrome hasta ahora. Hice esto porque Firefox y Chrome tienen diferentes implementaciones de API de HTML5 DND. (arrastrar y soltar).
Realmente espero que esto ayude a algunas personas.
Lo resolví con un tiempo de espera (no chirriante, pero funciona):
var dropTarget = $(''.dropTarget''),
html = $(''html''),
showDrag = false,
timeout = -1;
html.bind(''dragenter'', function () {
dropTarget.addClass(''dragging'');
showDrag = true;
});
html.bind(''dragover'', function(){
showDrag = true;
});
html.bind(''dragleave'', function (e) {
showDrag = false;
clearTimeout( timeout );
timeout = setTimeout( function(){
if( !showDrag ){ dropTarget.removeClass(''dragging''); }
}, 200 );
});
Mi ejemplo usa jQuery, pero no es necesario. Aquí hay un resumen de lo que está pasando:
- Establezca un indicador (
showDrag
) entrue
endragenter
ydragover
del elemento html (o cuerpo). - En
dragleave
establece la bandera enfalse
. A continuación, establezca un breve tiempo de espera para comprobar si el indicador sigue siendo falso. - Lo ideal es hacer un seguimiento del tiempo de espera y borrarlo antes de configurar el siguiente.
De esta manera, cada evento de dragleave
le da al DOM tiempo suficiente para que un nuevo evento dragover
reinicie la bandera. El verdadero dragleave
final que nos importa verá que la bandera sigue siendo falsa.
No sé, esto funciona para todos los casos, pero en mi caso funcionó muy bien.
$(''body'').bind("dragleave", function(e) {
if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
//outside body / window
}
});
Su tercer argumento para addEventListener
es true
, lo que hace que el oyente se ejecute durante la fase de captura (vea http://www.w3.org/TR/DOM-Level-3-Events/#event-flow para una visualización). Esto significa que capturará los eventos destinados a sus descendientes, y para el cuerpo que significa todos los elementos de la página. En tus manejadores, tendrás que comprobar si el elemento por el que se activan es el cuerpo mismo. Te daré mi forma muy sucia de hacerlo. Si alguien conoce una forma más sencilla que realmente compara elementos , me encantaría verlo.
this.dragenter = function() {
if ($(''body'').not(this).length != 0) return;
... functional code ...
}
Esto encuentra el cuerpo y lo elimina del conjunto de elementos encontrados. Si el conjunto no está vacío, this
no era el cuerpo, por lo que no nos gusta y regresamos. Si this
es body
, el conjunto estará vacío y el código se ejecutará.
Puedes probar con un simple if (this == $(''body'').get(0))
, pero probablemente fallará miserablemente.
Yo mismo tuve problemas con esto y se me ocurrió una solución utilizable, aunque no estoy loca por tener que usar una superposición.
Agregue ondragover
, ondragleave
y ondrop
a la ventana
Agregue ondragenter
, ondragleave
y ondrop
a una superposición y un elemento objetivo
Si se produce una caída en la ventana o superposición, se ignora, mientras que el objetivo maneja la caída como se desee. La razón por la que necesitamos una superposición es porque ondragleave
dispara cada vez que se ondragleave
un elemento, por lo que la superposición evita que esto suceda, mientras que a la zona de ondragleave
se le otorga un índice z más alto para que se puedan eliminar los archivos. Estoy usando algunos fragmentos de código que se encuentran en otras preguntas relacionadas con arrastrar y soltar, por lo que no puedo tomar todo el crédito. Aquí está el HTML completo:
<!DOCTYPE html>
<html>
<head>
<title>Drag and Drop Test</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<style>
#overlay {
display: none;
left: 0;
position: absolute;
top: 0;
z-index: 100;
}
#drop-zone {
background-color: #e0e9f1;
display: none;
font-size: 2em;
padding: 10px 0;
position: relative;
text-align: center;
z-index: 150;
}
#drop-zone.hover {
background-color: #b1c9dd;
}
output {
bottom: 10px;
left: 10px;
position: absolute;
}
</style>
<script>
var windowInitialized = false;
var overlayInitialized = false;
var dropZoneInitialized = false;
function handleFileSelect(e) {
e.preventDefault();
var files = e.dataTransfer.files;
var output = [];
for (var i = 0; i < files.length; i++) {
output.push(''<li>'',
''<strong>'', escape(files[i].name), ''</strong> ('', files[i].type || ''n/a'', '') - '',
files[i].size, '' bytes, last modified: '',
files[i].lastModifiedDate ? files[i].lastModifiedDate.toLocaleDateString() : ''n/a'',
''</li>'');
}
document.getElementById(''list'').innerHTML = ''<ul>'' + output.join('''') + ''</ul>'';
}
window.onload = function () {
var overlay = document.getElementById(''overlay'');
var dropZone = document.getElementById(''drop-zone'');
dropZone.ondragenter = function () {
dropZoneInitialized = true;
dropZone.className = ''hover'';
};
dropZone.ondragleave = function () {
dropZoneInitialized = false;
dropZone.className = '''';
};
dropZone.ondrop = function (e) {
handleFileSelect(e);
dropZoneInitialized = false;
dropZone.className = '''';
};
overlay.style.width = (window.innerWidth || document.body.clientWidth) + ''px'';
overlay.style.height = (window.innerHeight || document.body.clientHeight) + ''px'';
overlay.ondragenter = function () {
if (overlayInitialized) {
return;
}
overlayInitialized = true;
};
overlay.ondragleave = function () {
if (!dropZoneInitialized) {
dropZone.style.display = ''none'';
}
overlayInitialized = false;
};
overlay.ondrop = function (e) {
e.preventDefault();
dropZone.style.display = ''none'';
};
window.ondragover = function (e) {
e.preventDefault();
if (windowInitialized) {
return;
}
windowInitialized = true;
overlay.style.display = ''block'';
dropZone.style.display = ''block'';
};
window.ondragleave = function () {
if (!overlayInitialized && !dropZoneInitialized) {
windowInitialized = false;
overlay.style.display = ''none'';
dropZone.style.display = ''none'';
}
};
window.ondrop = function (e) {
e.preventDefault();
windowInitialized = false;
overlayInitialized = false;
dropZoneInitialized = false;
overlay.style.display = ''none'';
dropZone.style.display = ''none'';
};
};
</script>
</head>
<body>
<div id="overlay"></div>
<div id="drop-zone">Drop files here</div>
<output id="list"><output>
</body>
</html>