Arrastar de HTML5 acionado ao passar o elemento filho

O problema que estou tendo é que o evento dragleave de um elemento é triggersdo ao passar o elemento filho desse elemento. Além disso, o dragenter não é triggersdo ao passar o elemento pai novamente.

Eu fiz um violino simplificado: http://jsfiddle.net/pimvdb/HU6Mk/1/ .

HTML:

 
drag me

com o seguinte 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"); } }); 

O que é suposto fazer é notificar o usuário, fazendo o drop div vermelho ao arrastar algo para lá. Isso funciona, mas se você arrastar para o filho p , a dragleave é acionada e a div não é mais vermelha. Voltando para o drop div também não fica vermelho novamente. É necessário se mover completamente para fora da div e arrastá-lo novamente para torná-lo vermelho.

É possível impedir o dragleave de arrastar ao arrastar para um elemento filho?

Atualização de 2017: TL; DR, Pesquisar pointer-events: none; CSS pointer-events: none; como descrito na resposta do @ HD abaixo, que funciona nos navegadores modernos e no IE11.

Você só precisa manter um contador de referência, incrementá-lo quando obter um dragenter, decrementar quando obter um dragfleave. Quando o contador está em 0 – remova a 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'); } } }); 

Nota: No evento de queda, redefina o contador para zero e limpe a class adicionada.

Você pode rodar aqui

É possível impedir o acionamento de arrastar ao arrastar para um elemento filho?

Sim.

 #drop * {pointer-events: none;} 

Esse CSS parece ser suficiente para o Chrome.

Ao usá-lo com o Firefox, o #drop não deve ter nós de texto diretamente (caso contrário, há um problema estranho em que um elemento “deixe isso para si” ), então sugiro deixá-lo com apenas um elemento (por exemplo, usar div #drop para colocar tudo dentro

Aqui está um jsfiddle resolvendo o exemplo original (quebrado) .

Eu também fiz uma versão simplificada bifurcada pelo exemplo do @Theodore Brown, mas baseada somente neste CSS.

Nem todos os navegadores têm esse CSS implementado, no entanto: http://caniuse.com/pointer-events

Vendo o código-fonte do Facebook, encontrei este pointer-events: none; várias vezes, no entanto, é provavelmente usado junto com fallbacks de degradação graciosa. Pelo menos é tão simples e resolve o problema para muitos ambientes.

Aqui, a mais simples solução Cross-Browser (seriamente):

jsfiddle <- tente arrastar algum arquivo dentro da caixa

Você pode fazer algo assim:

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

Em poucas palavras, você cria uma “máscara” dentro da zona de salto, com largura e altura herdadas, posição absoluta, que mostrará apenas quando o dragover começar.
Então, depois de mostrar essa máscara, você pode fazer o truque, anexando os outros events dragleave & drop nele.

Depois de sair ou cair, você apenas esconde a máscara novamente.
Simples e sem complicações.

(Obs .: conselho de Greg Pettit – Você deve ter certeza de que a máscara passa por cima da checkbox inteira, incluindo a borda)

A maneira “certa” de resolver esse problema é desabilitar events de ponteiro em elementos filho do destino de descarte (como na resposta do @ HD). Aqui está um jsFiddle que criei, que demonstra essa técnica . Infelizmente, isso não funciona nas versões do Internet Explorer anteriores ao IE11, já que eles não suportavam events de ponteiro .

Felizmente, consegui criar uma solução alternativa que funciona em versões antigas do IE. Basicamente, envolve a identificação e ignorar events de dragleave que ocorrem ao arrastar sobre elementos filhos. Como o evento dragenter é acionado nos nós filhos antes do evento dragleave no pai, ouvintes de events separados podem ser adicionados a cada nó filho que adiciona ou remove uma class “ignorar-arrastar-deixar” do destino de soltar. Em seguida, o ouvinte de evento dragleave do destino de dragleave pode simplesmente ignorar as chamadas que ocorrem quando essa class existe. Aqui está um jsFiddle demonstrando essa solução alternativa . Ele é testado e funciona no Chrome, Firefox e IE8 +.

Atualizar:

Eu criei um jsFiddle demonstrando uma solução combinada usando a detecção de resources, onde os events de ponteiro são usados ​​se suportados (atualmente Chrome, Firefox e IE11) e o navegador volta a adicionar events a nós filhos se o suporte ao evento de ponteiro não estiver disponível -10).

Já passou algum tempo depois que esta pergunta é feita e muitas soluções (incluindo hacks feios) são fornecidas.

Consegui consertar o mesmo problema que tive recentemente graças à resposta desta resposta e achei que pode ser útil para alguém que chega a esta página. A idéia é armazenar o evenet.target no ondrageenter toda vez que ele for chamado em qualquer um dos elementos pai ou filho. Em seguida, no ondragleave verifique se o destino atual ( event.target ) é igual ao object que você armazenou no ondragenter .

O único caso em que esses dois são correspondidos é quando o arrastar está saindo da janela do navegador.

A razão pela qual isso funciona bem é quando o mouse deixa um elemento (digamos el1 ) e entra em outro elemento (digamos, el2 ), primeiro o el1.ondragenter é chamado e então el2.ondragleave . Somente quando o arrasto estiver saindo / entrando na janela do navegador, event.target será '' em el1.ondragenter e el2.ondragleave .

Aqui está minha amostra de trabalho. Eu testei no IE9 +, Chrome, Firefox e 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; }; })(); 

E aqui está uma página simples em HTML:

     Multiple File Uploader    
blah blah

Com o estilo adequado, o que fiz foi tornar o div interno (# file-drop-area) muito maior sempre que um arquivo é arrastado para a canvas, de modo que o usuário possa facilmente deixar os arquivos no local adequado.

O problema é que o evento dragleave está sendo acionado quando o mouse passa na frente do elemento filho.

Eu tentei vários methods de verificação para ver se o elemento e.target é o mesmo que o elemento this , mas não consegui nenhuma melhoria.

A maneira como consertei esse problema foi um pouco problemática, mas funciona 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(ex > rect.left + rect.width || ex < rect.left || ey > rect.top + rect.height || ey < rect.top) { $(this).removeClass('red'); } } 

Você pode corrigi-lo no Firefox com um pouco de inspiração do código fonte do jQuery :

 dragleave: function(e) { var related = e.relatedTarget, inside = false; if (related !== this) { if (related) { inside = jQuery.contains(this, related); } if (!inside) { $(this).removeClass('red'); } } } 

Infelizmente, isso não funciona no Chrome porque o relatedTarget parece não existir em events de dragleave e, presumo que você esteja trabalhando no Chrome, porque seu exemplo não funcionou no Firefox. Aqui está uma versão com o código acima implementado.

Uma solução muito simples é usar a propriedade CSS pointer-events . Apenas defina seu valor como none após dragstart em cada elemento filho. Esses elementos não ativarão mais os events relacionados ao mouse, de modo que eles não passarão o mouse sobre eles e, portanto, não acionarão o arrastar do pai.

Não se esqueça de definir essa propriedade de volta para auto ao terminar o arrasto;)

E aqui vai, uma solução para o 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 (ex > rect.left + rect.width - 1 || ex < rect.left || ey > rect.top + rect.height - 1 || ey < rect.top) { console.log('Drag is really out of area!'); } }) 

Aqui está outra solução usando 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'); } } 

Espero que isso funcione, aqui está um violino .

Eu escrevi uma pequena biblioteca chamada Dragster para lidar com esse problema exato, funciona em todos os lugares, exceto fazer silenciosamente nada no IE (que não suporta DOM Event Constructors, mas seria muito fácil escrever algo semelhante usando events customizados do jQuery)

Não tenho certeza se este cross browser, mas testei no Chrome e resolve o meu problema:

Eu quero arrastar e soltar um arquivo em toda a página, mas meu arrastar é acionado quando eu arrasto elemento filho. Minha correção foi olhar para o xey do mouse:

Eu tenho um div que sobrepõe toda a minha página, quando a página carrega eu escondê-lo.

quando você arrasta sobre o documento eu o mostro, e quando você solta no pai ele o manipula, e quando você deixa o pai eu verifico x e 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; }); 

Eu estava tendo o mesmo problema e tentei usar a solução pk7s. Funcionou, mas poderia ser feito um pouco melhor sem elementos extras.

Basicamente, a ideia é a mesma – adicione uma sobreposição extra invisível sobre a área que pode ser solta. Só vamos fazer isso sem elementos extras. Aqui está a parte em que os pseudo-elementos CSS chegam ao jogo.

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

Essa regra depois criará uma sobreposição totalmente coberta para a área que pode ser solta.

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

Aqui está a solução completa: http://jsfiddle.net/F6GDq/8/

Espero que ajude alguém com o mesmo problema.

Eu tropecei no mesmo problema e aqui está a minha solução – que eu acho que é muito mais fácil do que acima. Eu não tenho certeza se é crossbrowser (pode depender de ordem mesmo borbulhando)

Vou usar o jQuery para simplificar, mas a solução deve ser independente do framework.

O evento borbulha para ser pai, seja como for:

 
Parent Child

Eu prendo 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) 

E é sobre isso. 🙂 ele funciona porque, mesmo que onEnter em incêndios de crianças antes de onLeave no pai, atrasamos um pouco para reverter o pedido, então a class é removida primeiro e depois reaplicada após um milisegundo.

Apenas verifique se o elemento arrastado é um filho, se for, então não remova sua class de estilo ‘dragover’. Muito simples e funciona para mim:

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

Uma solução de trabalho alternativa, um pouco mais simples.

 //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) { 

Depois de passar tantas horas, consegui que a sugestão funcionasse exatamente como pretendido. Eu queria fornecer uma sugestão apenas quando os arquivos foram arrastados, e documentar o “dragover”, o “dragleave” estava causando falhas no navegador Chrome.

Foi assim que eu resolvi isso, também lançando pistas adequadas para o usuário.

 $(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 }); 

Eu escrevi um módulo de arrastar e soltar chamado drip-drop que corrige esse comportamento estranho, entre outros. Se você está procurando um bom módulo de arrastar e soltar de baixo nível que você pode usar como base para qualquer coisa (upload de arquivo, arrastar e soltar no aplicativo, arrastar de ou para fonts externas), você deve verificar isso módulo fora:

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

É assim que você faria o que está tentando fazer no drop-down:

 $('#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') } }) }) 

Para fazer isso sem uma biblioteca, a técnica de contagem é o que eu usei no drip-drop, embora a resposta mais bem avaliada não cumpra as etapas importantes que farão com que as coisas sejam interrompidas por tudo, exceto pela primeira queda. Veja como fazer isso corretamente:

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

Eu tive um problema semelhante – meu código para ocultar a zona de suspensão no evento arrastar e acionar o corpo foi triggersdo de forma contagiante quando pairava elementos filho fazendo a cintilação da zona de flutuação no Google Chrome.

Consegui resolver isso agendando a function para ocultar o dropzone em vez de chamá-lo imediatamente. Em seguida, se outro dragover ou dragfleave for triggersdo, a chamada de function agendada será cancelada.

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

Se você estiver usando HTML5, poderá obter o clientRect do pai:

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

Então no parent.dragleave ():

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

aqui é um jsfiddle

Uma solução simples é adicionar os pointer-events: none regra css pointer-events: none ao componente filho para evitar o acionamento do ondragleave . Consultar exemplo:

 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

Aqui está outra abordagem baseada no timing dos events.

O evento dragenter despachado do elemento filho pode ser capturado pelo elemento pai e sempre ocorre antes do dragleave . O tempo entre esses dois events é realmente curto, mais curto do que qualquer ação de mouse humana possível. Então, a idéia é memorizar o momento em que um dragenter acontece e filtrar os events de dragleave que ocorrem “não muito rapidamente” após …

Este pequeno exemplo funciona no Chrome e no 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 } }) 

pimvdb ..

Por que você não tenta usar o drop ao invés do dragleave ? Isso funcionou para mim. Espero que isso resolva seu problema.

Por favor, verifique o 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"); } }); 

You can use a timeout with a transitioning flag and listen on the top element. dragenter / dragleave from child events will bubble up to the container.

Since dragenter on the child element fires before dragleave of the container, we will set the flag show as transitioning for 1ms… the dragleave listener will check for the flag before the 1ms is up.

The flag will be true only during transitions to child elements, and will not be true when transitioning to a parent element (of the container)

 var $el = $('#drop-container'), transitioning = false; $el.on('dragenter', function(e) { // temporarily set the transitioning flag for 1 ms transitioning = true; setTimeout(function() { transitioning = false; }, 1); $el.toggleClass('dragging', true); e.preventDefault(); e.stopPropagation(); }); // dragleave fires immediately after dragenter, before 1ms timeout $el.on('dragleave', function(e) { // check for transitioning flag to determine if were transitioning to a child element // if not transitioning, we are leaving the container element if (transitioning === false) { $el.toggleClass('dragging', false); } e.preventDefault(); e.stopPropagation(); }); // to allow drop event listener to work $el.on('dragover', function(e) { e.preventDefault(); e.stopPropagation(); }); $el.on('drop', function(e) { alert("drop!"); }); 

jsfiddle: http://jsfiddle.net/ilovett/U7mJj/

You need to remove the pointer events for all child objects of the drag target.

 function disableChildPointerEvents(targetObj) { var cList = parentObj.childNodes for (i = 0; i < cList.length; ++i) { try{ cList[i].style.pointerEvents = 'none' if (cList[i].hasChildNodes()) disableChildPointerEvents(cList[i]) } catch (err) { // } } } 

use this code http://jsfiddle.net/HU6Mk/258/ :

 $('#drop').bind({ dragenter: function() { $(this).addClass('red'); }, dragleave: function(event) { var x = event.clientX, y = event.clientY, elementMouseIsOver = document.elementFromPoint(x, y); if(!$(elementMouseIsOver).closest('.red').length) { $(this).removeClass('red'); } } }); 

Just try to use the event.eventPhase. It will set to 2 (Event.AT_TARGET) only if the target ist entered, otherwise it is set to 3 (Event.BUBBLING_PHASE).

I’fe used the eventPhase to bind or unbind the dragleave Event.

 $('.dropzone').on('dragenter', function(e) { if(e.eventPhase === Event.AT_TARGET) { $('.dropzone').addClass('drag-over'); $('.dropzone').on('dragleave', function(e) { $('.dropzone').removeClass('drag-over'); }); }else{ $('.dropzone').off('dragleave'); } }) 

Guido

I found a similar but more elegant solution to @azlar’s answer, and this is my solution:

 $(document).on({ dragenter: function(e) { e.stopPropagation(); e.preventDefault(); $("#dragging").show(); }, dragover: function(e) { e.stopPropagation(); e.preventDefault(); }, dragleave: function(e) { e.stopPropagation(); e.preventDefault(); if (e.clientX <= 0 || // compare clientX with the width of browser viewport e.clientX >= $(window).width() || e.clientY <= 0 || e.clientY >= $(window).height()) $("#dragging").hide(); } }); 

This method detects whether the mouse has left the page. It works well in Chrome and Edge.