web-dev-qa-db-fra.com

"Dragleave" de l'élément parent se déclenche lorsqu'il glisse sur des éléments enfants

Vue d'ensemble

J'ai la structure HTML suivante et j'ai attaché les événements dragenter et dragleave à l'élément <div id="dropzone">.

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

Problème

Lorsque je fais glisser un fichier sur le <div id="dropzone">, l'événement dragenter est déclenché comme prévu. Toutefois, lorsque je déplace la souris sur un élément enfant, tel que <div id="drag-n-drop">, l'événement dragenter est déclenché pour l'élément <div id="drag-n-drop">, puis l'événement dragleave est déclenché pour l'élément <div id="dropzone">.

Si je survole de nouveau l'élément <div id="dropzone">, l'événement dragenter est à nouveau déclenché, ce qui est cool, mais l'événement dragleave est déclenché pour l'élément enfant qui vient d'être quitté. L'instruction removeClass est donc exécutée, ce qui n'est pas cool.

Ce comportement est problématique pour 2 raisons:

  1. Je ne fais que joindre dragenter & dragleave au <div id="dropzone">, donc je ne comprends pas pourquoi les éléments enfants ont également ces événements attachés.

  2. Je traîne toujours sur l'élément <div id="dropzone"> tout en survolant ses enfants, donc je ne veux pas que dragleave tire!

jsFiddle

Voici un jsFiddle à bricoler avec: http://jsfiddle.net/yYF3S/2/

Question

Alors ... comment puis-je faire en sorte que lorsque je glisse un fichier sur l'élément <div id="dropzone">, dragleave ne se déclenche pas, même si je traîne sur des éléments enfants ... il ne devrait se déclencher que lorsque je quitte le <div id="dropzone"> element ... survoler/déplacer n'importe où dans les limites de l'élément ne devrait pas déclencher l'événement dragleave.

J'ai besoin que cela soit compatible avec tous les navigateurs, du moins dans les navigateurs prenant en charge HTML5 glisser-déposer, donc cette réponse n'est pas adéquat.

Il semble que Google et Dropbox aient compris cela, mais leur code source est complexe/complexe, aussi je n’ai pas été en mesure de le comprendre à partir de leur implémentation.

146
Hristo

J'ai finalement trouvé une solution qui me satisfait. En fait, j’ai trouvé plusieurs façons de faire ce que je veux, mais aucun n’a eu le même succès que la solution actuelle. Dans une solution, j’ai fréquemment clignoté à la suite de l’ajout/suppression d’une bordure à l’élément #dropzone ... dans une autre, le La bordure n'a jamais été supprimée si vous vous éloignez du navigateur.

Quoi qu'il en soit, ma meilleure solution de hacky est la suivante:

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

Cela fonctionne plutôt bien, mais des problèmes se sont posés dans Firefox car Firefox invoquant la variable dragenter, le compteur était désactivé. Mais néanmoins, ce n'est pas une solution très élégante.

Puis je suis tombé sur cette question: Comment détecter l’événement dragleave dans Firefox lorsqu’il glisse hors de la fenêtre

Alors j'ai pris la réponse et l'ai appliquée à ma 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;
    }
});

C'est propre et élégant et semble fonctionner dans tous les navigateurs que j'ai testés jusqu'à présent (pas encore testé IE).

52
Hristo

Si vous n'avez pas besoin de lier des événements aux éléments enfants, vous pouvez toujours utiliser la propriété pointer-events.

.child-elements {
  pointer-events: none;
}
152
Ben Rudolph

C'est un peu moche mais ça marche sacrément! ...

Sur votre gestionnaire "dragenter", stockez le fichier event.target (dans une variable de votre fermeture, ou autre), puis dans votre gestionnaire "dragleave", déclenchez votre code uniquement si event.target === celui que vous avez stocké.

Si votre "dragenter" se déclenche alors que vous ne le souhaitez pas (c'est-à-dire qu'il entre après avoir laissé des éléments enfants), la dernière fois qu'il se déclenche avant que la souris ne quitte le parent, c'est sur le parent, de sorte que le parent sera toujours le "dragenter" final avant le "dragleave" prévu.

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

}());
35
hacklikecrack

Au début, j’ai accepté que les gens abandonnent l’approche pointer-events: none. Mais ensuite je me suis demandé:

Avez-vous vraiment besoin que des événements de pointeur travaillent sur les éléments enfants alors que le glissement est en cours

Dans mon cas, il y a beaucoup de choses qui se passent chez les enfants, par exemple. survolez pour afficher les boutons pour des actions supplémentaires, l'édition en ligne, etc ... Cependant, aucun de ce qui est nécessaire ou même souhaité pendant un glisser.

Dans mon cas, j'utilise quelque chose comme ceci pour désactiver les événements de pointeur de manière sélective pour tous les nœuds enfants du conteneur parent:

  div.drag-target-parent-container.dragging-in-progress * {
    pointer-events: none;
  }

Utilisez votre approche préférée pour ajouter/supprimer la classe dragging-in-progress dans les gestionnaires d'événements dragEnter/dragLeave, comme je l'ai fait ou faire de même dans dragStart, et. Al.

21
bargar

Cela semble être un bug de Chrome.

La seule solution à laquelle je pouvais penser était de créer un élément de recouvrement transparent pour capturer vos événements: http://jsfiddle.net/yYFFSS/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>
​
12
Blender

Le problème est que vos éléments dans les zones de transfert font bien sûr partie de la zone de transfert et lorsque vous entrez les enfants, vous quittez le parent. Résoudre ce n'est pas facile. Vous pouvez essayer d’ajouter des événements aux enfants et d’ajouter votre classe au parent.

$("#dropzone,#dropzone *").on('dragenter', function(event) {

    // add a class to #dropzone

    event.stopPropagation(); // might not be necessary
    event.preventDefault();
    return false;
});

Vos événements seront toujours déclenchés à plusieurs reprises, mais personne ne verra.

// Edit: utilisez l'événement dragmove pour écraser définitivement l'événement dragleave:

$("#dropzone,#dropzone *").on('dragenter dragover', function(event) {

    // add a class to #dropzone

    event.stopPropagation(); // might not be necessary
    event.preventDefault();
    return false;
});

Définissez l'événement dragleave uniquement pour la zone de dépôt.

5
Oliver

Comme benr mentionné dans cette réponse , vous pouvez empêcher les nœuds enfants de se déclencher sur des événements, mais si vous devez lier certains événements, procédez comme suit:

#dropzone.dragover *{
   pointer-events: none;
}

Et ajoutez celui-ci à votre code JS:

$("#dropzone").on("dragover", function (event) {
   $("#dropzone").addClass("dragover");
});

$("#dropzone").on("dragleave", function (event) {
   $("#dropzone").removeClass("dragover");
});
3
Vahid Ashrafian

Si vous utilisez jQuery, vérifiez ceci: https://github.com/dancork/jquery.event.dragout

C'est vraiment génial.

Evénement spécial créé pour gérer les fonctionnalités véritables de glisser-déposer.

L'événement dragleave HTML5 fonctionne plus comme mouseout. Ce plugin était créé pour reproduire la fonctionnalité de style mouseleave tandis que traînant.

Exemple d'utilisation:

$ ('# myelement'). on ('dragout', function (event) { // VOTRE CODE });

EDIT: en fait, je ne pense pas que cela dépende de jQuery, vous pouvez probablement simplement utiliser le code même sans celui-ci.

3
nkkollaw

@hristo j'ai une solution beaucoup plus élégante. Vérifiez que si c'est quelque chose que vous pourriez utiliser.

Vos efforts n'ont pas été vains après tout. J'ai réussi à utiliser le vôtre au début, mais j'avais des problèmes différents dans FF, Chrome. Après avoir passé tant d’heures, cette suggestion a fonctionné exactement comme prévu.

Voici comment cela se passe. J'ai également profité des repères visuels pour guider correctement les utilisateurs à propos de la zone de largage.

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

Mes deux cents: 

Démo: 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;
});
2
jeremie.b

Solution simple et rapide pour cela, non testée de manière approfondie mais fonctionnant sous Chrome pour le moment.

Excusez le 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

Cela corrige le bogue de scintillement de Chrome, ou du moins sa permutation qui me causait des problèmes.

1
Dom Vinyard

Donc, pour moi, l'approche pointer-events: none; n'a pas très bien fonctionné ... Voici donc ma solution alternative:

    #dropzone {
        position: relative;
    }

    #dropzone(.active)::after {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        content: '';
    }

De cette façon, il n'est pas possible de dragleave le parent (sur un enfant) ni dragover un élément enfant. J'espère que cela t'aides :)

* La classe '.active' que j'ajoute quand dragenter ou dragleave. Mais si vous travaillez sans cela, laissez simplement la classe à distance.

1
MMachinegun

J'essayais de l'implémenter moi-même pour une boîte de téléchargement de fichier dans laquelle la couleur de la boîte changerait lorsque les utilisateurs feraient glisser un fichier dans l'espace.

J'ai trouvé une solution qui consiste en un joli mélange de Javascript et de CSS. Supposons que vous avez une zone de dépôt avec div avec l'ID #drop. Ajoutez ceci à votre Javascript:

$('#drop').on('dragenter', function() {
    $(this).addClass('dragover');
    $(this).children().addClass('inactive');
});

$('#drop').on('dragleave', function() {
    $(this).removeClass('dragover');
    $(this).children().removeClass('inactive');
});

Ensuite, ajoutez ceci à votre CSS pour désactiver tous les enfants qui vont classer .inactive:

#drop *.inactive {
    pointer-events: none;
}

Ainsi, les éléments enfants resteront inactifs tant que l'utilisateur le fera glisser sur la boîte.

0
David A

J'ai eu un problème similaire et je l'ai corrigé de cette façon:

Le problème: La fonction drop (ev) est déclenchée lorsque l'utilisateur dépose un élément dans la "zone de largage" (élément ul), mais malheureusement aussi lorsque l'élément est déposé dans l'un de ses enfants (éléments li) .

Le correctif:

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

Cette réponse peut être trouvée ici:

Dragleave HTML5 déclenché lors du survol d'un élément enfant

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');
        }
    }
});
0
zeros-and-ones

En fait, j'aime bien ce que je vois sur https://github.com/lolmaus/jquery.dragbetter/ mais je voulais partager une alternative possible .. Ma stratégie générale consistait à appliquer un style de fond à la zone de dépôt. (pas ses enfants) quand dragirant ou un de ses enfants (via des bulles). Ensuite, je supprime le style en faisant glisser la zone de dépôt. L'idée était de passer à un enfant, même si je supprime le style de la zone de dépôt lorsque je le quitte (le feutre du glisser-déposer), je voudrais simplement réappliquer le style à la zone de dépôt parent lorsque dragentering est actif. Le problème est bien sûr que lorsqu’on passe de la zone de dépôt à un enfant de la zone de dépôt, le dragenter est déclenché sur l’enfant avant le glisser-feu, de sorte que mes styles ont été appliqués dans le désordre. La solution pour moi consistait à utiliser une minuterie pour forcer l'événement dragenter à revenir dans la file d'attente des messages, ce qui me permettait de le traiter APRÈS le glisser-glisser. J'ai utilisé une fermeture pour accéder à l'événement sur le rappel du minuteur.

$('.dropzone').on('dragenter', function(event) {
  (function (event) {
    setTimeout(function () {
      $(event.target).closest('.dropzone').addClass('highlight');
    }, 0);
  }) (event.originalEvent); 
});

Cela semble fonctionner en chrome, c.-à-d. Firefox et fonctionne quel que soit le nombre d'enfants dans la zone de baisse. Je suis un peu inquiet au sujet du délai d'attente garantissant la réorganisation des événements, mais cela semble assez bien fonctionner pour mon cas d'utilisation.

0
mefopolis

Je ne me sentais pas satisfait des solutions de contournement présentées ici, car je ne voulais pas perdre le contrôle des éléments enfants.

J'ai donc utilisé une approche logique différente, traduisant celle-ci en un plugin jQuery, appelé jquery-draghandler . Il ne manipule absolument pas le DOM, garantissant des performances élevées. Son utilisation est simple:

$(document).ready(function() {

    $(selector).draghandler({
        onDragEnter: function() {
            // $(this).doSomething();
        },
        onDragLeave: function() {
            // $(this).doSomethingElse();
        }
    });

});

Il traite parfaitement le problème sans compromettre les fonctionnalités du DOM.

Télécharger, détails et explications sur son dépôt Git .

0
Black Shell

Ici, une des solutions les plus simples ● ︿ ●

Jetez un oeil à ceci violon <- essayez de faire glisser un fichier

Vous pouvez faire quelque chose comme ça:

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

Par là, vous devriez déjà savoir ce qui se passe ici.
Regardez le violon, vous savez.

0

J'essayais de le mettre en œuvre moi-même et je ne voulais pas non plus Jquery ni aucun plugin. 

Je voulais gérer le téléchargement de fichier, de la manière suivante qui me convenait le mieux:

Structure des fichiers: 

---/uploads {répertoire uploads}

--- /js/slyupload.js {fichier javascript.}

--- index.php

--- upload.php

--- styles.css {juste un peu de style ..}

Code HTML:

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

Alors ceci est le fichier Javascript attaché :: '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;
}

0
Andaeiii

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

Avec cette mise en page:

<div class="dropzone">
  <div class="inner-zone">Inner-zone</div>
</div>

J'ai effectué un dump de classes d'éléments pour e.target, e.currentTarget, e.relatedTarget pour les deux événements dragover et dragleave.

Il m’a montré qu’en quittant le bloc parent (.dropzone), e.relatedTarget n’est pas un enfant de ce bloc, je sais donc que je suis hors du Dropzone.

0
mortalis