A arrastar em HTML5 é acionada ao passar o mouse sobre um elemento filho


300

O problema que estou tendo é que o dragleaveevento de um elemento é acionado ao passar o mouse sobre um elemento filho desse elemento. Além disso, dragenternão é acionado ao passar novamente o elemento pai.

Fiz um violino simplificado: 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>

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 ele deve fazer é notificar o usuário, deixando a gota divvermelha ao arrastar algo para lá. Isso funciona, mas se você arrastar a pcriança, ela dragleaveé acionada e divnão fica mais vermelha. Voltar para a gota divtambém não a torna vermelha novamente. É necessário sair completamente da gota dive arrastá-la novamente para torná-la vermelha.

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

Atualização de 2017: TL; DR, consulte CSS, pointer-events: none;conforme descrito na resposta da @ HD abaixo, que funciona em navegadores modernos e no IE11.


O bug relatado pelo pimvdb ainda existe no Webkit a partir de maio de 2012. Eu o contratei adicionando também uma classe no dragover, que não é nada agradável, pois é acionada com tanta frequência, mas parece corrigir um pouco o problema.
ajm

2
@ajm: Obrigado, isso funciona até certo ponto. No entanto, no Chrome, há um flash ao entrar ou sair do elemento filho, provavelmente porque dragleaveainda é acionado nesse caso.
Pimvdb

Eu abri um bug jQuery UI upvotes são bem-vindos para que eles possam decidir colocar recursos nele
fguillen

@guguenen: Me desculpe, mas isso não tem nada a ver com a interface do jQuery. De fato, o jQuery nem é necessário para acionar o bug. Já registrei um bug do WebKit, mas não há atualizações a partir de agora.
Pimvdb

2
@pimvdb Por que você não aceita a resposta do HD?
precisa saber é o seguinte

Respostas:


338

Você só precisa manter um contador de referência, incrementá-lo quando obtém um dragenter, diminuir quando obtém um dragleave. Quando o contador estiver em 0 - remova a classe.

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 drop, redefina o contador para zero e limpe a classe adicionada.

Você pode executá-lo aqui


8
OMG, esta é a solução mais óbvia e só teve UM voto ... Vamos lá pessoal, você pode fazer melhor. Eu estava pensando sobre isso, mas depois de ver o nível de sofisticação das primeiras respostas, quase o descartei. Você teve alguma desvantagem?
Arthur Corenzan

11
Isso não funcionou quando a borda do elemento que está sendo arrastado toca a borda de outro elemento arrastável. Por exemplo, uma lista classificável; arrastando o elemento para baixo, sobre o próximo item arrastável não diminui o contador de volta para 0, mas fica preso em 1. Mas se eu arrastá-lo para o lado, de qualquer outro elemento arrastável, ele funciona. Eu fui com pointer-events: noneas crianças. Quando começo a arrastar, anexo uma classe que tem essa propriedade. Quando arrasto, removo a classe. Funcionou bem no Safari e Chrome, mas não no Firefox.
Arthur Corenzan 14/03/2015

13
Isso funcionou para mim em todos os navegadores, mas no Firefox. Eu tenho uma nova solução que funciona em qualquer lugar no meu caso. No primeiro dragentereu salvo event.currentTargetem uma nova variável dragEnterTarget. Enquanto dragEnterTargetestiver definido, ignoro outros dragentereventos, porque são de crianças. Em todos os dragleaveeventos eu verifico dragEnterTarget === event.target. Se isso for falso, o evento será ignorado, pois foi disparado por uma criança. Se isso for verdade, redefinir dragEnterTargetpara undefined.
Pipo

3
Ótima solução. Eu precisava redefinir o contador para 0 na função que lida com a aceitação da queda, caso contrário, os arrastamentos subsequentes não funcionariam conforme o esperado.
Liam Johnston

2
@ Woody, eu não vi o comentário dele na enorme lista de comentários aqui, mas sim, essa é a solução. Por que não incorporar isso na sua resposta?
BT

93

É possível impedir que o dragleave seja acionado 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 "deixa para si" ), então sugiro deixá-lo com apenas um elemento (por exemplo, use uma div dentro #drop para colocar tudo dentro)

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

Também fiz uma versão simplificada do exemplo do @Theodore Brown, mas baseada apenas neste CSS.

Porém, nem todos os navegadores têm esse CSS implementado: http://caniuse.com/pointer-events

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


A propriedade pointer-events é a solução certa no futuro, mas infelizmente não funciona no IE8-IE10, que ainda é amplamente utilizado. Além disso, devo salientar que seu jsFiddle atual nem funciona no IE11, pois não inclui os ouvintes de eventos necessários e a prevenção de comportamento padrão.
Theodore Brown

2
Usar eventos-ponteiro é realmente uma boa resposta, lutei um pouco antes de descobrir por mim mesmo que essa resposta deveria ser maior.
Floribon

Opção fantástica para aqueles de nós que tiveram a sorte de não precisar suportar navegadores antigos.
Luke Hansford

7
E se seus filhos forem um botão? oO
David

9
isso só funciona se você não tem outros elementos do controlador (editar, apagar) dentro da área, porque esta solução bloqueia-los também ..
Zoltán Süle

45

Aqui, a solução mais simples entre navegadores (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 drop-down, com largura e altura herdadas, posição absoluta, que será exibida apenas quando a dragover iniciar.
Então, depois de mostrar essa máscara, você pode fazer o truque anexando os outros eventos de arrastar e soltar nela.

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

(Obs .: conselhos de Greg Pettit - você deve ter certeza de que a máscara passa a caixa inteira, incluindo a borda)


Não sei por que, mas não está funcionando de maneira consistente com o Chrome. Às vezes, sair da área mantém a máscara visível.
Greg Pettit

2
Na verdade, é a fronteira. Tenha a máscara sobreposta à borda, sem borda própria, e deve funcionar bem.
Greg Pettit

1
Observe que o seu jsfiddle possui um bug. No drag_drop, você deve remover a classe hover em "#box" e não "# box-a"
entropia

Esta é uma boa solução, mas de alguma forma não funcionou para mim. Eu finalmente descobri uma solução alternativa. Para aqueles que procuram algo mais. você pode tentar isso: github.com/bingjie2680/jquery-draghover
bingjie2680

Não. Não quero perder o controle sobre os elementos filhos. Eu criei um plugin jQuery simples que lida com o problema de fazer o trabalho por nós. Veja minha resposta .
Luca Fagioli

39

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

Consegui corrigir o mesmo problema que tive recentemente graças à resposta nesta resposta e achei que poderia ser útil para alguém que acessa esta página. A idéia é armazenar o evenet.targetem ondrageentertoda ela é chamada em qualquer um dos elementos pai ou filho. Em seguida, ondragleaveverifique se o destino atual ( event.target) é igual ao objeto em que você armazenou ondragenter.

O único caso de correspondência desses dois é quando o arrasto sai da janela do navegador.

A razão pela qual isso funciona bem é quando o mouse sai de um elemento (digamos el1) e entra em outro elemento (digamos el2), primeiro el2.ondragenteré chamado e depois el1.ondragleave. Somente quando o arrasto estiver saindo / entrando na janela do navegador, event.targetestará ''em ambos el2.ondragenter e el1.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 html simples:

<!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>

Com um estilo adequado, o que eu fiz foi aumentar a div interna (# área de queda de arquivos) sempre que um arquivo é arrastado para a tela, para que o usuário possa soltar os arquivos facilmente no local apropriado.


4
Essa é a melhor solução, é melhor que o contador (especialmente se você delegar eventos) e também funciona com crianças arrastáveis.
Fred

3
Isto não ajuda tho para o problema de eventos dragenter duplicados
BT

Isso funciona para "filhos" que são pseudoelementos ou pseudo-classes css? Não consegui entender, mas talvez estivesse fazendo errado.
Josh Bleecher Snyder

1
Em março de 2020, tive a melhor sorte com esta solução também. Nota: alguns linters podem reclamar sobre o uso de ==over ===. Você está comparando as referências de objeto de destino do evento, portanto também ===funciona bem.
Alex

33

A maneira "certa" de resolver esse problema é desabilitar os eventos do ponteiro nos elementos filhos 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, pois elas não suportam eventos de ponteiro .

Felizmente, eu era capaz de chegar a uma solução que faz o trabalho em versões antigas do IE. Basicamente, envolve identificar e ignorar dragleaveeventos que ocorrem ao arrastar sobre elementos filho. Como o dragenterevento é disparado nos nós filhos antes do dragleaveevento no pai, ouvintes de eventos separados podem ser adicionados a cada nó filho que adiciona ou remove uma classe "ignore-drag-leave" do destino da gota. Em seguida, o dragleaveouvinte de evento do destino de recebimento pode simplesmente ignorar as chamadas que ocorrem quando essa classe existe. Aqui está um jsFiddle demonstrando essa solução alternativa . Ele foi testado e funciona no Chrome, Firefox e IE8 +.

Atualizar:

Criei um jsFiddle demonstrando uma solução combinada usando detecção de recurso, em que eventos de ponteiro são usados ​​se suportados (atualmente Chrome, Firefox e IE11) e o navegador volta a adicionar eventos a nós filhos se o suporte a evento de ponteiro não estiver disponível (IE8 -10).


Esta resposta mostra uma solução alternativa para o disparo indesejável, mas ignora a pergunta "É possível impedir que o dragleave seja acionado ao arrastar para um elemento filho?" inteiramente.
HD

O comportamento pode ficar estranho ao arrastar um arquivo de fora do navegador. No Firefox, eu tenho "Entrando filho -> Entrando pai -> Sair filho -> Entrando filho -> Sair filho" sem sair do pai, que saiu com a classe "over". O IE antigo precisaria de uma substituição de attachEvent para o addEventListener.
HD

Essa solução depende fortemente da bolha, o "falso" em todos os addEventListener deve ser enfatizado como essencial (embora esse seja o comportamento padrão), pois muitas pessoas podem não saber disso.
HD

Essa única arrastável adiciona um efeito ao dropzone que não aparece quando o dropzone é usado para outros objetos arrastáveis ​​que não acionam o evento dragstart. Talvez usar tudo como um dropzone para o efeito de arrastar enquanto mantém o dropzone real com outros manipuladores faria isso.
HD

1
@HD Atualizei minha resposta com informações sobre o uso da propriedade CSS pointer-events para impedir que o dragleaveevento seja disparado no Chrome, Firefox e IE11 +. Também atualizei minha outra solução alternativa para dar suporte ao IE8 e IE9, além do IE10. É intencional que o efeito da zona de lançamento seja adicionado apenas ao arrastar o link "Arraste-me". Outros podem ficar à vontade para alterar esse comportamento, conforme necessário, para dar suporte a seus casos de uso.
Theodore Brown

14

O problema é que o dragleaveevento está sendo disparado quando o mouse fica na frente do elemento filho.

Eu tentei vários métodos de verificação para ver se o e.targetelemento é igual ao thiselemento, mas não obtive nenhuma melhoria.

A maneira como corrigi esse problema foi um pouco complicado, 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(e.x > rect.left + rect.width || e.x < rect.left
               || e.y > rect.top + rect.height || e.y < rect.top) {
                   $(this).removeClass('red');
               }
           }

1
Obrigado! No entanto, não consigo fazer isso funcionar no Chrome. Você poderia fornecer uma amostra do seu hack?
Pimvdb

3
Eu estava pensando em fazer isso verificando as cordas também. Você fez a maior parte do trabalho para mim, thx :). Eu tive que fazer alguns ajustes, no entanto:if (e.x >= (rect.left + rect.width) || e.x <= rect.left || e.y >= (rect.top + rect.height) || e.y <= rect.top)
Christof

1
Não funcionará no Chrome porque seu evento não tem e.xe e.y.
Hengjie 01/09/2013

Eu gosto desta solução. Não funcionou no Firefox primeiro. Mas se você substituir ex por e.clientX e ey por e.clientY, ele funcionará. Também funciona no Chrome.
Nicole Stutz

Não funcionou em cromo para mim, nem fez o que Chris ou Daniel Stuts sugeriu
Timo Huovinen

14

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

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

Em seguida, 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 está um jsfiddle


2
Excelente resposta.
Broc.seib 31/01

Obrigado amigo, eu gosto da abordagem javascript simples.
Tim Gerhard

Ao usar elementos que possuem border-radius, mover o ponteiro para perto do canto deixaria o elemento, mas esse código ainda pensaria que estamos dentro (deixamos o elemento, mas ainda estamos no retângulo delimitador). Em seguida, o dragleavemanipulador de eventos não será chamado.
Jay Dadhania

11

Uma solução muito simples é usar a pointer-eventspropriedade CSS . Basta definir o seu valor para nonecima dragstart em cada elemento filho. Esses elementos não acionam mais os eventos relacionados ao mouse; portanto, eles não pegam o mouse sobre eles e, portanto, não acionam a saída de arrastar no pai.

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


11

Esta solução bastante simples está funcionando para mim até agora, supondo que seu evento esteja anexado a cada elemento de arrastar individualmente.

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

Isso é ótimo! obrigado por compartilhar @kenneth, você salvou meu dia! Muitas das outras respostas se aplicam apenas se você tiver uma única zona descartável.
antoni

Para mim, ele funciona no Chrome e Firefox, mas o id não funciona no Edge. Por favor, veja: jsfiddle.net/iwiss/t9pv24jo
iwis 25/03

8

Solução muito simples:

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

Isso não parece funcionar no Safari 12.1: jsfiddle.net/6d0qc87m
Adam Taylor

7

Você pode corrigi-lo no Firefox com um pouco de inspiração no 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, ele não funciona no Chrome porque relatedTargetparece não existir em dragleaveeventos, e suponho 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.


Muito obrigado, mas, de fato, é no Chrome que estou tentando resolver esse problema.
pimvdb

5
@pimvdb Vejo que você registrou um erro , deixarei aqui uma referência a ele, caso alguém encontre essa resposta.
robertc

Eu realmente fiz, mas esqueci de adicionar um link aqui. Obrigado por fazer isso.
Pimvdb

O bug do Chrome foi corrigido nesse meio tempo.
Phd #

7

E aqui está 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 (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!');
                    }
                })

Ele entra em «if (tipo de evento.clienteX === 'indefinido')»?
Aldekein

1
Funcionou bem, mas pode haver outra janela no navegador, portanto, não é suficiente obter a localização do mouse e compará-la com a área retangular da tela.
HD

Concordo com @HD Além disso, isso causará problemas quando o elemento for grande, border-radiuscomo expliquei no meu comentário na resposta do @ azlar acima.
Jay Dadhania

7

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 .


3
Isso não funcionou para mim imediatamente. Eu precisava usar event.clientX, event.clientY, pois eles são relativos à janela de visualização e não à página. Espero que ajude outra alma perdida por aí.
Jon Abrams

7

Uma solução simples é adicionar a regra css pointer-events: noneao componente filho para impedir o acionamento de 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;
}
<article draggable="true">drag me</article>

<div ondragenter="enter(event)" ondragleave="leave(event)">
  drop here
  <p>child not triggering dragleave</p>
</div>


Eu tive esse mesmo problema e sua solução funciona muito bem. Dica para outras pessoas: esse foi o meu caso: se o elemento filho que você está tentando ignorar o evento drag é o clone do elemento que está sendo arrastado (tentando obter uma visualização visual), você pode usá-lo dentro do dragstart ao criar o clone :dragged = event.target;clone = dragged.cloneNode();clone.style.pointerEvents = 'none';
Scaramouche

4

Não tenho certeza se este navegador cruzado, mas eu testei no Chrome e resolve o meu problema:

Quero arrastar e soltar um arquivo por toda a página, mas minha saída de arrastar é acionada quando arrasto sobre o elemento filho. Minha correção foi olhar o xey do mouse:

Eu tenho uma div que se sobrepõe a minha página inteira, quando a página carrega eu a oculto.

quando você arrasta o documento, eu o mostro; quando você solta o pai, ele lida com ele; e quando você deixa o pai, verifica 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;
});

1
Hacky, mas inteligente. Eu gosto. Trabalhou para mim.
Cupcakekid

1
Oh, como eu gostaria que esta resposta tivesse mais votos! Obrigado
Kirby

4

Eu escrevi uma pequena biblioteca chamada Dragster para lidar com esse problema exato, funciona em qualquer lugar, exceto silenciosamente, sem fazer nada no IE (que não suporta Construtores de Eventos DOM, mas seria muito fácil escrever algo semelhante usando os eventos personalizados do jQuery)


Muito útil (pelo menos para mim, onde eu me importo apenas com o Chrome).
M Katz

4

Eu estava tendo o mesmo problema e tentei usar a solução pk7s. Funcionou, mas poderia ser feito um pouco melhor sem nenhum elemento extra de dom.

Basicamente, a idéia é a mesma - adicione uma sobreposição invisível extra sobre a área de droppable. Só vamos fazer isso sem nenhum elemento extra de dom. Aqui está a parte em que os pseudoelementos CSS aparecem.

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 posterior criará uma sobreposição totalmente coberta para a área descartável.

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


1
Ocasionalmente não funciona no Chrome (não pegar dragleave corretamente)
Timo Huovinen

4

Apenas verifique se o elemento arrastado é um filho, se for, não remova sua classe 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');
      }
  })

Parece a solução mais simples para mim e resolveu o meu problema. Obrigado!
Francesco Marchetti-Stasi

3

Eu me deparei com o mesmo problema e aqui está a minha solução - que eu acho que é muito mais fácil do que acima. Não tenho certeza se é crossbrowser (pode depender até de ordem borbulhante)

Usarei o jQuery para simplificar, mas a solução deve ser independente da estrutura.

O evento borbulha para o pai de qualquer maneira, conforme indicado:

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

Anexamos eventos

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. :) funciona porque, embora onEnter no filho seja acionado antes de onLeave no pai, atrasamos um pouco a reversão da ordem, para que a classe seja removida primeiro e depois reaplicada após um milissegundo.


A única coisa que esse snippet faz é impedir que a classe 'hovered' seja removida, reaplicando-a no próximo ciclo de ticks (tornando o evento 'dragleave' inútil).
Nulo

Não é inútil. Se você deixar o pai, ele funcionará como esperado. O poder desta solução é sua simplicidade, não é o ideal ou o melhor que existe. Uma solução melhor seria marcar um enter em uma criança, verificar novamente se nós apenas inserimos child e, se não, acionar o evento leave. Vai hovever necessidade de testar, guardas extras, checing para os netos, etc.
Marcin Raczkowski

3

Eu escrevi um módulo de arrastar e soltar chamado drip-drop que corrige esse comportamento esquisito, entre outros. Se você estiver procurando por um bom módulo de arrastar e soltar de baixo nível que possa ser usado como base para qualquer coisa (upload de arquivo, arrastar e soltar no aplicativo, arrastar de ou para fontes externas), verifique isso saída do módulo:

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

É assim que você faria o que está tentando fazer no 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')
    }
  })
})

Para fazer isso sem uma biblioteca, a técnica do contador é o que eu usei no drip-drop, embora a resposta mais alta perca etapas importantes que farão com que as coisas quebrem por tudo, exceto a primeira gota. Veja como fazê-lo 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
    }
});

2

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

Parece que nem sempre funciona no Chrome. Ocasionalmente, recebia clientXacima de 0 quando o mouse está fora da caixa. Concedido, meus elementos sãoposition:absolute
Hengjie

Isso acontece o tempo todo ou apenas algumas vezes? Porque se o mouse estiver se movendo muito rápido (por exemplo, fora da janela), você poderá obter valores errados.
Profet

Isso acontece 90% do tempo. Existem casos raros (1 em 10 vezes) em que consigo chegar a 0. Tentarei novamente mover o mouse mais devagar, mas não posso dizer que estava me movendo rapidamente (talvez o que você chamaria de velocidade normal).
Hengjie 02/02

2

Depois de passar tantas horas, recebi a sugestão funcionando exatamente como pretendido. Eu queria fornecer uma dica apenas quando os arquivos eram arrastados e a passagem de documentos, a saída de arrasto estava causando tremulações dolorosas no navegador Chrome.

Foi assim que eu resolvi, também dando dicas 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
});

1

Eu tive um problema semelhante - meu código para ocultar o dropzone no evento dragleave for body foi disparado contatantemente quando pairava elementos filho, fazendo o dropzone piscar no Google Chrome.

Consegui resolver isso agendando a função para ocultar o dropzone em vez de chamá-lo imediatamente. Em seguida, se outro arrastar ou sair de arrastar for acionado, a chamada de função 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);

Foi isso que acabei fazendo também, mas ainda é incompleto.
MPEN

1

O evento " dragleave " é acionado quando o ponteiro do mouse sai da área de arraste do contêiner de destino.

O que faz muito sentido, pois, em muitos casos, apenas os pais podem ser descartáveis e não os descendentes. Acho event.stopPropogation () deveria ter lidado com esse caso, mas parece que não funciona.

Acima mencionado, algumas soluções parecem funcionar na maioria dos casos, mas falham no caso dos filhos que não suportam eventos dragenter / dragleave , como iframe .

Uma solução alternativa é verificar o event.relatedTarget e verificar se ele reside dentro do contêiner, depois ignore o evento dragleave como fiz aqui:

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

Você pode encontrar um violino trabalhando aqui !


1

Resolvido ..!

Declare qualquer matriz para 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');
}

Não precisa se preocupar com elementos filho.


1

Eu lutei muito com isso, mesmo depois de ler todas essas respostas, e pensei em compartilhar minha solução com você, porque achei que poderia ser uma das abordagens mais simples, embora um pouco diferente. Meu pensamento era simplesmente omitir dragleavecompletamente o ouvinte de evento e codificar o comportamento da saída de arrastar com cada novo evento dragenter acionado, enquanto certificava-me de que os eventos dragenter não fossem acionados desnecessariamente.

No meu exemplo abaixo, eu tenho uma tabela na qual desejo trocar o conteúdo da linha da tabela por meio da API de arrastar e soltar. Em dragenter, uma classe CSS deve ser adicionada ao elemento de linha no qual você está arrastando o elemento para destacá-lo e dragleave, em seguida , essa classe deve ser removida.

Exemplo:

Tabela HTML muito básica:

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

E a função dragenter manipulador de eventos, adicionado para cada célula da tabela (com exceção dragstart, dragover, drop, e dragendmanipuladores, que não são específicos para esta questão, por isso não copiadas aqui):

/*##############################################################################
##                              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;
  }
}

Comentários muito básicos deixados no código, de modo que este código seja adequado também para iniciantes. Espero que isso ajude você! Observe que é claro que você precisará adicionar todos os ouvintes de eventos mencionados acima em cada célula da tabela para que isso funcione.


0

Aqui está outra abordagem baseada no tempo dos eventos.

O dragenterevento despachado do elemento filho pode ser capturado pelo elemento pai e sempre ocorre antes do dragleave. O tempo entre esses dois eventos é realmente curto, mais curto do que qualquer ação possível do mouse humano. Portanto, a idéia é memorizar o horário em que dragenteracontece e filtrar os dragleaveeventos que ocorrem "não muito rapidamente" após ...

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

0

pimvdb ..

Por que você não tenta usar drop em vez de arrastar . Funcionou para mim. Espero que isso resolva seu problema.

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

2
Se o usuário mudar de idéia e arrastar o arquivo de volta para fora do navegador, isso permanecerá vermelho.
ZachB

0

Você pode usar um tempo limite com uma transitioningbandeira e ouvir o elemento superior. dragenter/ dragleavede eventos filhos borbulharão até o contêiner.

Como dragenterno elemento filho dispara antes dragleavedo contêiner, definiremos o sinalizador como transição para 1ms ... o dragleaveouvinte verificará o sinalizador antes que o 1ms esteja ativo.

O sinalizador será verdadeiro apenas durante as transições para elementos filho e não será verdadeiro ao fazer a transição para um elemento pai (do contêiner)

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/


0

Você precisa remover os eventos do ponteiro para todos os objetos filhos do destino de arrastar.

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) {
                //
            }
        }
    }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.