"dragleave" родительского элемента срабатывает при перетаскивании дочерних элементов

Обзор

У меня есть следующая структура HTML, и я привязал события dragenter и dragleave к элементу <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>

Проблема

Когда я перетаскиваю файл по <div id="dropzone">, событие dragenter запускается, как ожидалось. Однако, когда я перемещаю мышь над дочерним элементом, например <div id="drag-n-drop">, событие dragenter запускается для элемента <div id="drag-n-drop">, а затем событие dragleave запускается для элемента <div id="dropzone">.

Если я снова накрою элемент <div id="dropzone">, событие dragenter снова будет запущено, это круто, но затем событие dragleave будет запущено для дочернего элемента слева, поэтому команда removeClass выполненный, что не круто.

Это поведение проблематично по двум причинам:

  • Я привязываю dragenter и dragleave к <div id="dropzone">, поэтому я не понимаю, почему эти элементы также связаны с дочерними элементами.

  • Я по-прежнему перетаскиваю элемент <div id="dropzone"> во время зависания над его дочерними элементами, поэтому я не хочу, чтобы dragleave срабатывал!

jsFiddle

Здесь jsFiddle, чтобы возиться с: http://jsfiddle.net/yYF3S/2/

Вопрос

Итак... как я могу сделать так, что когда я перетаскиваю файл над элементом <div id="dropzone">, dragleave не срабатывает, даже если я перетаскиваю любые дочерние элементы... он должен только огонь, когда я покидаю элемент <div id="dropzone">... зависание/перетаскивание в любом месте границ элемента не должно запускать событие dragleave.

Мне нужно, чтобы это было совместимо с несколькими браузерами, по крайней мере, в браузерах, поддерживающих drag-n-drop в HTML5, поэтому этот ответ не подходит.

Похоже, Google и Dropbox выдумали это, но их исходный код минимизирован/сложный, поэтому я не смог понять это из их реализации.

Ответ 1

Наконец-то я нашел решение, которым я доволен. Я действительно нашел несколько способов сделать то, что хочу, но ни один из них не был столь успешным, как текущее решение... в одном решении я часто испытывал мерцание в результате добавления/удаления границы с элементом #dropzone... в другом, граница не удалялась, если вы отскакиваете от браузера.

В любом случае, мое лучшее хакерское решение таково:

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

Это работает очень хорошо, но проблемы возникли в Firefox, потому что Firefox дважды вызывал dragenter, поэтому мой счетчик был выключен. Но, тем не менее, это не очень изящное решение.

Затем я наткнулся на этот вопрос: Как определить событие dragleave в Firefox при перетаскивании за пределы окна

Итак, я принял ответ и применил его к моей ситуации:

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

Это чисто и элегантно и, кажется, работает в каждом браузере, который я тестировал до сих пор (еще не тестировал IE).

Ответ 2

Если вам не нужно привязывать события к дочерним элементам, вы всегда можете использовать свойство указателей-событий.

.child-elements {
  pointer-events: none;
}

Ответ 3

Это немного уродливо, но это работает черт!..

В вашем обработчике dragenter хранится event.target(в переменной внутри вашего закрытия или что-то еще), тогда в вашем обработчике "dragleave" запускается только ваш код, если event.target === тот, который вы сохранили.

Если ваш "dragenter" запускается, когда вы этого не хотите (т.е. когда он вводится после выхода из дочерних элементов), то последний раз, когда он запускается до того, как мышь покинет родительский элемент, он находится у родителя, поэтому родительский элемент всегда будет конечным "драгунтером" перед предполагаемым "dragleave".

(function () {

    var droppable = $('#droppable'),
        lastenter;

    droppable.on("dragenter", function (event) {
        lastenter = event.target;
        droppable.addClass("drag-over");            
    });

    droppable.on("dragleave", function (event) {
        if (lastenter === event.target) {
            droppable.removeClass("drag-over");
        }
    });

}());

Ответ 4

Сначала я согласился с тем, что люди отказываются от подхода pointer-events: none. Но потом я спросил себя:

Вам действительно нужны указатели-события для работы над дочерними элементами при перетаскивании?

В моем случае у меня много вещей, которые происходят у детей, например. hover, чтобы отображать кнопки для дополнительных действий, встроенное редактирование и т.д. Однако none того, что необходимо или даже даже желательно во время перетаскивания.

В моем случае я использую что-то подобное, чтобы выборочно отключать события указателя для всех дочерних узлов родительского контейнера:

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

Используйте свой любимый подход для добавления/удаления класса dragging-in-progress в обработчиках событий dragEnter/dragLeave, так же как я сделал или сделаю то же самое в dragStart, et. и др.

Ответ 5

Кажется, это ошибка Chrome.

Единственным обходным решением, которое я мог подумать, было создание прозрачного элемента overlay для захвата ваших событий: http://jsfiddle.net/yYF3S/10/

JS

$(document).ready(function() {
    var dropzone = $('#overlay');

    dropzone.on('dragenter', function(event) {
        $('#dropzone-highlight').addClass('dnd-hover');
    });

    dropzone.on('dragleave', function(event) {
        $('#dropzone-highlight').removeClass('dnd-hover');
    });

});​

HTML

<div id="dropzone-highlight">
    <div id="overlay"></div>

    <div id="dropzone" class="zone">
        <div id="drag-n-drop">
            <div class="text1">this is some text</div>
            <div class="text2">this is a container with text and images</div>
        </div>
    </div>
</div>

<h2 draggable="true">Drag me</h2>
​

Ответ 6

Проблема в том, что ваши элементы внутри dropzones, конечно, являются частью dropzone, и когда вы вводите детей, вы оставляете родителя. Решение этого непросто. Вы можете попробовать добавить события к детям, снова добавив свой класс к родительскому.

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

    // add a class to #dropzone

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

Ваши события будут срабатывать многократно, но никто не увидит.

//Правка: используйте событие dragmove, чтобы навсегда перезаписать событие dragleave:

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

    // add a class to #dropzone

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

Определите событие dragleave только для dropzone.

Ответ 7

Как benr, упомянутый в этом ответе, вы можете предотвратить запуск дочерних узлов на события, но если вам нужно связать некоторые события, сделайте следующее:

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

И добавьте это в свой JS-код:

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

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

Ответ 8

Если вы используете jQuery, проверьте это: https://github.com/dancork/jquery.event.dragout

Это действительно потрясающе.

Специальное событие, созданное для обработки истинных функций dragleave.

Событие HTML5 dragleave больше похоже на mouseout. Этот плагин был созданный для репликации функциональности стиля mouseleave в то время как волочение.

Пример использования:

$( '# MyElement'). На ( 'dragout', функция (событие) {     // ВАШ КОД });

EDIT: на самом деле, я не думаю, что он зависит от jQuery, вы можете просто использовать код даже без него.

Ответ 9

@hristo У меня есть гораздо более элегантное решение. Убедитесь, что это то, что вы могли бы использовать.

В конце концов, ваши усилия не пропали даром. Сначала мне удалось использовать ваши, но у меня были разные проблемы в FF, Chrome. Проведя так много часов, я получил это предложение, работающее точно так, как предполагалось.

Вот как реализована реализация. Я также воспользовался визуальными подсказками для правильного указания пользователей о зоне выпадения.

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

Ответ 10

Мои два цента: Скройте слой над вашим dropzone, а затем покажите его, когда вы перетащите его, и наведите на него dragleave.

Демо: 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;
});

Ответ 11

Итак, для меня подход pointer-events: none; не работал слишком хорошо... Итак, вот мое альтернативное решение:

    #dropzone {
        position: relative;
    }

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

Таким образом, невозможно dragleave родительский (дочерний элемент) или dragover дочерний элемент. надеюсь, это поможет:)

* ".active" класс добавляется при я dragenter или dragleave. Но если вы работаете без этого, просто покиньте класс.

Ответ 12

Супер простое быстрое решение для этого, не проверено широко, но работает в Chrome прямо сейчас.

Извините 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

Это исправляет ошибку фликкера Chrome или, по крайней мере, его перестановку, которая вызывала у меня проблемы.

Ответ 13

Моя версия:

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

С этим расположением:

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

Я сделал дамп классов элементов для e.target, e.currentTarget, e.relatedTarget для событий dragover и dragleave.

Он показал мне, что при выходе из родительского блока (.dropzone) e.relatedTarget не является дочерним элементом этого блока, поэтому я знаю, что я вышел из dropzone.

Ответ 14

Здесь одно из самых простых решений ● ︿ ●

Взгляните на это fiddle < - попробуйте перетащить некоторый файл в поле

Вы можете сделать что-то вроде этого:

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

Тем самым вы уже должны знать, что здесь происходит. Знаете, просто взгляните на скрипку.

Ответ 15

У меня была аналогичная проблема, и я исправил ее следующим образом:

Проблема: Функция drop (ev) запускается, когда пользователь бросает элемент в "зону отбрасывания" (элемент ul), но, к сожалению, также, когда элемент выпадает в один из его дочерних элементов (элементы li).

Исправление:

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

Ответ 16

Я пытался реализовать это самостоятельно для окна загрузки файлов, где цвет ящика изменился бы, когда пользователи перетащили файл в это пространство.

Я нашел решение, которое представляет собой приятное сочетание Javascript и CSS. Скажем, у вас есть зона сбрасывания с div с id #drop. Добавьте это в свой Javascript:

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

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

Затем добавьте это в свой CSS, чтобы инактивировать всех детей будет класс .inactive:

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

Таким образом, дочерние элементы будут неактивными до тех пор, пока пользователь перетаскивает элемент над полем.

Ответ 17

Я не был удовлетворен каким-либо из обходных решений, представленным здесь, потому что я не хочу потерять контроль над элементами children.

Итак, я использовал другой логический подход, переводя его в плагин jQuery, называемый jquery-draghandler. Он абсолютно не манипулирует DOM, гарантируя высокие показатели. Его использование прост:

$(document).ready(function() {

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

});

Он безупречно справляется с проблемой без ущерба для любых функциональных возможностей DOM.

Загрузите, детали и пояснения в Git репозитории.

Ответ 18

Я пытался реализовать это сам, и я не хотел Jquery или любой плагин либо.

Я хотел обработать загрузку файла следующим образом:

Структура файлов:

---/uploads {uploads directory}

---/js/slyupload.js {файл javascript.}

--- index.php

--- upload.php

--- styles.css {немного стайлинга..}

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>

Тогда это прикрепленный файл Javascript :: '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;
}

Ответ 19

Мне действительно нравится то, что я вижу в https://github.com/lolmaus/jquery.dragbetter/, но хотел поделиться возможной альтернативой. Моя общая стратегия заключалась в том, чтобы применить стиль фона к dropzone (а не его дочерним элементам) при перетаскивании его или любом ребенке (через пузырьки). Затем я удаляю стиль при отключении зоны выпадения. Идея заключалась в том, чтобы переходить к ребенку, даже если я удаляю стиль из dropzone при выходе из него (драглавные огни), я бы просто применил стиль к родительской dropzone при перетаскивании любого дочернего элемента. Проблема, конечно, в том, что при переходе от dropzone к ребенку из dropzone, драгетер обстреливает ребенка перед драглавеем, поэтому мои стили были применены не в порядке. Решение для меня состояло в том, чтобы использовать таймер, чтобы заставить событие dragenter вернуться в очередь сообщений, что позволило мне обработать его ПОСЛЕ dragleave. Я использовал закрытие для доступа к событию на обратном вызове таймера.

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

Это похоже на Chrome, т.е. firefox и работает независимо от количества детей в dropzone. Я немного обеспокоен таймаутом, гарантирующим переупорядочение событий, но, похоже, он очень хорошо подходит для моего использования.

Ответ 21

Извините, это javascript не jquery, но для меня это самый логичный способ решить эту проблему. Браузер должен вызвать dropleave (предыдущего элемента) перед dropenter (или новым элементом, поскольку что-то не может войти во что-то еще, прежде чем покинуть первое, я не понимаю, почему они это сделали! Так что вам просто нужно отложить de dropleave следующим образом:

function mydropleave(e)
{
    e.preventDefault();
    e.stopPropagation();

    setTimeout(function(e){ //the things you want to do },1);
}

И капельница случится после капли, вот и все!

Ответ 22

Используйте greedy : true для дочернего элемента как функцию droppable. Затем запустите только событие, нажатое на первый слой.