HTML5 dragleave срабатывает при наведении на дочерний элемент


301

У меня проблема в том, что dragleaveсобытие элемента вызывается при наведении на дочерний элемент этого элемента. Также dragenterне срабатывает при повторном наведении на родительский элемент.

Я сделал упрощенную скрипку: 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>

со следующим 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");
                 }
                });

Предполагается, что он должен уведомить пользователя, сделав его divкрасным при перетаскивании чего-либо туда. Это работает, но если вы перетаскиваете на pребенка, dragleaveон уволен и divбольше не красный. Возвращение к падению divтакже не делает его снова красным. Необходимо полностью выйти из капли divи снова перетащить в нее, чтобы она стала красной.

Можно ли предотвратить dragleaveвыстрел при перетаскивании в дочерний элемент?

Обновление 2017 года: TL; DR, найдите CSS, pointer-events: none;как описано в ответе @ HD ниже, который работает в современных браузерах и IE11.


Обнаруженная ошибка pimvdb все еще существует в Webkit по состоянию на май 2012 года. Я противостоял ей, также добавив класс в dragover, который не очень близок к nice, поскольку он запускается так часто, но, похоже, немного исправляет проблему.
— Ajm

2
@ajm: Спасибо, это работает до определенной степени. Однако в Chrome при входе или выходе из дочернего элемента возникает вспышка, возможно, потому что dragleaveв этом случае все еще срабатывает.
— pimvdb

Я открыл ошибку JQuery UI upvotes приветствуются , чтобы они могли решить поставить ресурсы на него
— fguillen

@fguillen: извините, но это не имеет ничего общего с jQuery UI. На самом деле, jQuery даже не нужен, чтобы вызвать ошибку. Я уже подал ошибку WebKit, но на данный момент обновлений нет.
— pimvdb

2
@pimvdb Почему вы не принимаете ответ HD?
— TylerH

Ответы:


339

Вам просто нужно сохранить счетчик ссылок, увеличивать его, когда вы получаете перетаскивание, уменьшать, когда вы получаете перетаскивание. Когда счетчик на 0 - удалить класс.

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

Примечание. В событии сброса сбросьте счетчик до нуля и очистите добавленный класс.

Вы можете запустить его здесь


8
О боже, это самое очевидное решение, и только один голос ... Давай люди, вы можете сделать лучше. Я думал об этом, но, увидев уровень сложности первых нескольких ответов, я почти отбросил его. Были ли у вас недостатки?
— Артур Корензан

11
Это не сработало, когда край перетаскиваемого элемента касается края другого перетаскиваемого элемента. Например, сортируемый список; перетаскивание элемента вниз по следующему перетаскиваемому элементу не уменьшает счетчик обратно до 0, вместо этого он застревает на 1. Но если я перетаскиваю его вбок, из любого другого перетаскиваемого элемента, это работает. Я пошел с pointer-events: noneдетьми. Когда я начинаю перетаскивать, я добавляю класс, у которого есть это свойство, когда перетаскивание заканчивается, я удаляю класс. Хорошо работал на Safari и Chrome, но не на Firefox.
— Артур Корензан

13
Это работало для меня во всех браузерах, кроме Firefox. У меня есть новое решение, которое работает везде в моем случае. На первом dragenterсохраняю event.currentTargetв новую переменную dragEnterTarget. Пока dragEnterTargetустановлено, я игнорирую дальнейшие dragenterсобытия, потому что они от детей. Во всех dragleaveсобытиях я проверяю dragEnterTarget === event.target. Если это false, событие будет проигнорировано, так как оно было запущено ребенком. Если это правда, я возвращаюсь dragEnterTargetк undefined.
— Пипо

3
Отличное решение. Мне нужно было сбросить счетчик до 0 в функции, которая обрабатывает принятие отбрасывания, в противном случае последующие перетаскивания не сработали, как ожидалось.
— Лиам Джонстон

2
@ Woody Я не видел его комментарий в огромном списке комментариев, но да, это исправление. Почему бы не включить это в свой ответ?
— BT

93

Можно ли предотвратить запуск Dragleave при перетаскивании в дочерний элемент?

Да.

#drop * {pointer-events: none;}

Этого CSS достаточно для Chrome.

При использовании его с Firefox, #drop не должно иметь текстовых узлов напрямую (иначе есть странная проблема, когда элемент «оставь это себе» ), поэтому я предлагаю оставить его только с одним элементом (например, использовать div внутри #drop положить все внутрь)

Вот jsfiddle, решающий оригинальный вопрос (сломанный) пример .

Я также сделал упрощенную версию разветвленной из примера @Theodore Brown, но основанную только на этом CSS.

Однако не во всех браузерах реализован этот CSS: http://caniuse.com/pointer-events

Увидев исходный код Facebook, я мог найти это pointer-events: none;несколько раз, но, вероятно, он использовался вместе с грациозным отказом от деградации. По крайней мере, это так просто и решает проблему для многих сред.


Свойство указателя-события является правильным решением в будущем, но, к сожалению, оно не работает в IE8-IE10, которые все еще широко используются. Кроме того, я должен отметить, что ваш текущий jsFiddle даже не работает в IE11, так как он не добавляет необходимые прослушиватели событий и предотвращает поведение по умолчанию.
— Теодор Браун

2
Использование событий указателя действительно хороший ответ, я немного потрудился, прежде чем сам выяснил, что ответ должен быть выше.
— Флорибон

Фантастический вариант для тех из нас, кому посчастливилось не поддерживать старые браузеры.
— Люк Хансфорд

7
Что если ваши дети - Баттон? oO
— Дэвид

9
это работает, только если у вас нет других элементов контроллера (редактирование, удаление) внутри области, потому что это решение также блокирует их ..
— Zoltán Süle

45

Здесь самое простое кросс-браузерное решение (серьезно):

jsfiddle <- попробуйте перетащить какой-нибудь файл в поле

Вы можете сделать что-то подобное:

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

В нескольких словах, вы создаете «маску» внутри зоны сброса, с унаследованной шириной и высотой, абсолютной позицией, которая будет отображаться только тогда, когда начинается драгувер.
Итак, после показа этой маски, вы можете сделать трюк, прикрепив к ней другие события Dragleave & Drop.

После ухода или падения вы просто снова прячете маску.
Просто и без осложнений.

(Обс .: совет Грега Петтита - Вы должны быть уверены, что маска нависает над всей коробкой, включая границы)


Не знаю почему, но он не работает согласованно с Chrome. Иногда при выходе из зоны маска остается видимой.
— Грег Петтит

2
На самом деле, это граница. Имейте маску, перекрывающую границу, без собственной границы, и она должна работать хорошо.
— Грег Петтит

1
Обратите внимание, что в вашем jsfiddle есть ошибка, в drag_drop вы должны удалить класс наведения на «#box», а не «# box-a»
— энтропия

Это хорошее решение, но у меня как-то не получилось. Я наконец-то нашел обходной путь. Для тех из вас, кто ищет что-то еще. Вы можете попробовать это: github.com/bingjie2680/jquery-draghover
— bingjie2680

Нет, я не хочу терять контроль над дочерними элементами. Я сделал простой плагин jQuery, который решает проблему, выполняя работу за нас. Проверьте мой ответ .
— Лука Фагиоли

39

Прошло довольно много времени после того, как этот вопрос был задан, и было предложено множество решений (включая некрасивые хаки).

Мне удалось решить ту же проблему, с которой я столкнулся недавно благодаря ответу в этом ответе, и подумал, что это может быть полезно для тех, кто заходит на эту страницу. Вся идея заключается в том, чтобы хранить evenet.targetв ondrageenterкаждый раз, когда он вызывается на любом из родительских или дочерних элементов. Затем ondragleaveпроверьте, соответствует ли текущий target ( event.target) объекту, в котором вы храните ondragenter.

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

Причина, по которой это работает нормально, заключается в том, что мышь покидает элемент (скажем el1) и вводит другой элемент (скажем el2), сначала el2.ondragenterвызывается, а затем el1.ondragleave. Только когда перетаскивание покидает / входит в окно браузера, event.targetбудет ''в обоих el2.ondragenter и el1.ondragleave.

Вот мой рабочий образец. Я проверил это на IE9 +, Chrome, Firefox и 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;
    };
})();

А вот простая HTML-страница:

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

При правильном оформлении я сделал внутренний раздел (# file-drop-area) намного больше, когда файл перетаскивается на экран, чтобы пользователь мог легко поместить файлы в нужное место.


4
Это лучшее решение, оно лучше счетчика (особенно если вы делегируете события) и работает также с перетаскиваемыми дочерними элементами.
— Фред

3
Это не помогает в случае дублирования событий драгентера
— BT

Работает ли это для "детей", которые являются псевдоэлементами CSS или псевдоклассами? Я не мог получить это, но возможно я делал это неправильно.
— Джош Блихер Снайдер

1
По состоянию на март 2020 года мне больше всего повезло и с этим решением. Примечание: некоторые линтеры могут жаловаться на использование ==over ===. Вы сравниваете ссылки на целевой объект события, поэтому ===тоже отлично работает.
— Алекс

33

«Правильный» способ решить эту проблему - отключить события указателя на дочерних элементах цели удаления (как в ответе @ HD). Вот jsFiddle, который я создал, который демонстрирует эту технику . К сожалению, это не работает в версиях Internet Explorer до IE11, так как они не поддерживали события указателя .

К счастью, я был в состоянии придумать обходной путь , который делает работу в старых версиях IE. По сути, это включает в себя выявление и игнорирование dragleaveсобытий, которые происходят при перетаскивании на дочерние элементы. Поскольку dragenterсобытие вызывается на дочерних узлах до dragleaveсобытия на родительском узле, к каждому дочернему узлу можно добавить отдельных прослушивателей событий, которые добавляют или удаляют класс ignore-drag-left из цели удаления. Тогда dragleaveпрослушиватель события целевой цели может просто игнорировать вызовы, которые происходят, когда этот класс существует. Вот jsFiddle, демонстрирующий этот обходной путь . Он протестирован и работает в Chrome, Firefox и IE8 +.

Обновить:

Я создал jsFiddle, демонстрирующий комбинированное решение с использованием обнаружения функций, в котором используются события указателя, если они поддерживаются (в настоящее время Chrome, Firefox и IE11), и браузер возвращается к добавлению событий в дочерние узлы, если поддержка событий указателя недоступна (IE8 -10).


Этот ответ показывает обходной путь для нежелательного увольнения, но игнорирует вопрос "Можно ли предотвратить выстрел драгла при перетаскивании в дочерний элемент?" полностью.
— HD

Поведение может стать странным при перетаскивании файла из-за пределов браузера. В Firefox у меня есть «Ввод дочернего -> Ввод родителя -> Оставивший ребенка -> Ввод дочернего -> Выйти из ребенка», не оставляя родителя, который ушел с классом «за». Старому IE потребуется замена attachEvent для addEventListener.
— HD

Это решение сильно зависит от всплывающего окна, «ложь» во всех addEventListener должна быть подчеркнута как существенная (хотя это поведение по умолчанию), так как многие люди могут не знать об этом.
— HD

Этот единственный перетаскиваемый объект добавляет эффект к зоне перетаскивания, который не появляется, когда зона перетаскивания используется для других перетаскиваемых объектов, которая не вызывает событие перетаскивания. Возможно, использование всего этого в качестве рабочей зоны для эффекта перетаскивания при сохранении реальной рабочей зоны с другими обработчиками сделает это.
— HD

1
@HD Я обновил свой ответ информацией об использовании CSS-свойства pointer-events, чтобы предотвратить dragleaveзапуск события в Chrome, Firefox и IE11 +. Я также обновил свой другой обходной путь для поддержки IE8 и IE9, в дополнение к IE10. Намеренно, что эффект дропзоны добавляется только при перетаскивании ссылки «Перетащите меня». Другие могут свободно изменять это поведение по мере необходимости для поддержки своих вариантов использования.
— Теодор Браун

14

Проблема в том, что dragleaveсобытие вызывается, когда мышь находится перед дочерним элементом.

Я пробовал различные методы проверки, чтобы увидеть, e.targetсовпадает ли thisэлемент с элементом, но не смог добиться какого-либо улучшения.

Способ, которым я исправил эту проблему, был немного взломан, но работает на 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
Спасибо! Я не могу заставить это работать в Chrome как бы то ни было. Не могли бы вы предоставить рабочую скрипку своего хака?
— pimvdb

3
Я думал сделать это, проверяя координаты тоже. Ты сделал большую часть работы для меня, спасибо :). Я должен был сделать некоторые корректировки, хотя:if (e.x >= (rect.left + rect.width) || e.x <= rect.left || e.y >= (rect.top + rect.height) || e.y <= rect.top)
— Christof

1
Это не будет работать в Chrome, потому что это событие не имеет e.xи e.y.
— Hengjie

Мне нравится это решение. Сначала не работал в Firefox. Но если вы замените ex на e.clientX, а ey на e.clientY, то это сработает. Также работает в Chrome.
— Николь Штутц

Не работает в хроме для меня, и не сделал то , что Крис или Даниэль Stuts предложил
— Тимо Huovinen

14

если вы используете HTML5, вы можете получить clientRect родительского:

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

Затем в parent.dragleave ():

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

вот jsfiddle


2
Отличный ответ.
— broc.seib

Спасибо, приятель, мне нравится простой JavaScript-подход.
— Тим Герхард

При использовании на элементах, имеющих border-radius, перемещение указателя близко к углу фактически оставило бы элемент, но этот код все еще думал бы, что мы внутри (мы оставили элемент, но мы все еще находимся в ограничительном прямоугольнике). Тогда dragleaveобработчик событий вообще не будет вызываться.
— Джей Дадхания

11

Очень простое решение - использовать pointer-eventsсвойство CSS . Просто установите его значение noneна dragstart для каждого дочернего элемента. Эти элементы больше не будут вызывать связанные с мышью события, поэтому они не будут ловить мышь над ними и, следовательно, не будут вызывать перетаскивание на родительском элементе .

Не забудьте установить это свойство обратно autoпри завершении перетаскивания;)


11

Это довольно простое решение работает для меня до сих пор, при условии, что ваше событие прикреплено к каждому элементу перетаскивания индивидуально.

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

Это круто! спасибо, что поделились @kenneth, вы спасли мой день! Многие другие ответы применимы только в том случае, если у вас есть одна зона сбрасывания.
— Антони

Для меня это работает в Chrome и Firefox, но id не работает в Edge. Пожалуйста , смотрите: jsfiddle.net/iwiss/t9pv24jo
— IWIS


7

Вы можете исправить это в Firefox с небольшим вдохновением из исходного кода 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');
        }
    }

}

К сожалению, это не работает в Chrome, потому что, relatedTargetпохоже, не существует в dragleaveсобытиях, и я предполагаю, что вы работаете в Chrome, потому что ваш пример не работал в Firefox. Вот версия с реализованным выше кодом.


Большое спасибо, но на самом деле это Chrome, в котором я пытаюсь решить эту проблему.
— pimvdb

5
@pimvdb Я вижу, что вы зарегистрировали ошибку , я просто оставлю здесь ссылку на случай, если кто-нибудь еще столкнется с этим ответом.
— Робертc

Я действительно, но я забыл добавить ссылку на это здесь. Спасибо за это.
— pimvdb

В настоящее время ошибка Chrome была исправлена.
— phk

7

И вот оно, решение для 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!');
                    }
                })

Это входит в «if (typeof event.clientX === 'undefined')»?
— Aldekein

1
Работал хорошо, но в браузере могло быть другое окно, поэтому получить местоположение мыши и сравнить его с прямоугольной областью экрана недостаточно.
— HD

Я согласен с @HD. Кроме того, это вызовет проблемы, когда элемент имеет большой размер, border-radiusкак я объяснил в своем комментарии к ответу @ azlar выше.
— Джей Дадхания

7

Вот еще одно решение с использованием 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');
   }
}

Надеюсь, что это работает, вот скрипка .


3
Это не сработало для меня из коробки. Мне нужно было использовать event.clientX, event.clientYвместо этого, так как они относятся к области просмотра, а не к странице. Я надеюсь, что это помогает другой потерянной душе там.
— Джон Абрамс

7

Простое решение - добавить правило css pointer-events: noneв дочерний компонент, чтобы предотвратить срабатывание триггера ondragleave. Смотрите пример:

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>


У меня была такая же проблема, и ваше решение прекрасно работает. Совет для других, это был мой случай: если дочерний элемент, который вы пытаетесь игнорировать событие перетаскивания, является клоном перетаскиваемого элемента (пытаясь получить визуальный предварительный просмотр), вы можете использовать это внутри dragstart при создании клона :dragged = event.target;clone = dragged.cloneNode();clone.style.pointerEvents = 'none';
— Scaramouche

4

Не уверен, что это кросс-браузер, но я тестировал в Chrome, и это решает мою проблему:

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

У меня есть div, который перекрывает всю мою страницу, когда страница загружается, я скрываю это.

когда вы перетаскиваете документ, я показываю его, а когда вы опускаете родительский элемент, он обрабатывает его, а когда вы покидаете родительский элемент, я проверяю x и 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
Хаки, но умные. Мне нравиться. Работал на меня.
— Cupcakekid

1
О, как бы мне хотелось, чтобы за этот ответ было больше голосов! Спасибо
— Кирби

4

Я написал небольшую библиотеку Dragster для решения этой конкретной проблемы, которая работает везде, кроме тихого бездействия в IE (который не поддерживает конструкторы событий DOM, но было бы довольно легко написать нечто подобное с помощью пользовательских событий jQuery)


Очень полезно (по крайней мере, для меня, где я забочусь только о Chrome).
— М Кац

4

У меня возникла та же проблема, и я попытался использовать решение pk7s. Это сработало, но можно было бы сделать немного лучше без каких-либо дополнительных элементов DOM.

По сути, идея та же самая - добавить дополнительное невидимое наложение поверх области, которую можно опустить. Только давайте делать это без каких-либо дополнительных элементов DOM. Вот часть, где псевдоэлементы CSS вступают в игру.

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

Это правило после создания создаст полностью покрытый оверлей для области, которую можно сбросить.

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

Вот полное решение: http://jsfiddle.net/F6GDq/8/

Я надеюсь, что это поможет любому с той же проблемой.


1
Изредка не работает на хроме (не ловит драглэйв должным образом)
— Тимо Хуовинен,

4

Просто проверьте, является ли перетаскиваемый элемент дочерним, если это так, то не удаляйте свой класс стиля dragover. Довольно просто и работает для меня:

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

Похоже, самое простое решение для меня, и это решило мою проблему. Спасибо!
— Франческо Маркетти-Штази

3

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

Я буду использовать jQuery для простоты, но решение должно быть независимым от фреймворка.

Событие переходит к родителю в любом случае так:

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

Мы прикрепляем события

el = $('.parent')
setHover = function(){ el.addClass('hovered') }
onEnter  = function(){ setTimeout(setHover, 1) }
onLeave  = function(){ el.removeClass('hovered') } 
$('.parent').bind('dragenter', onEnter).bind('dragleave', onLeave)

И это все. :) это работает, потому что хотя onEnter на дочернем объекте срабатывает перед onLeave на родительском, мы немного задерживаем его, изменяя порядок, поэтому сначала класс удаляется, а затем повторно вызывается через миллисекунду.


Единственное, что делает этот фрагмент, это предотвращает удаление класса «hovered» путем его повторного применения в следующем цикле тиков (что делает событие «dragleave» бесполезным).
— ноль

Это не бесполезно. Если вы оставите родителя, он будет работать как положено. Сила этого решения в том, что оно простое, не идеальное и не лучшее. Лучшим решением было бы отметить вход на дочернем элементе, в проверке onleave, если мы только что ввели дочерний элемент, и если нет, вызвать событие отпускания. Тем не менее, он нуждается в проверке, дополнительной охране, проверке внуков и т. Д.
— Марчин Рачковски,

3

Я написал модуль drag-and-drop под названием drip-drop, который, среди прочего, исправляет это странное поведение. Если вы ищете хороший низкоуровневый модуль перетаскивания, который вы можете использовать в качестве основы для чего-либо (загрузка файлов, перетаскивание в приложении, перетаскивание из или на внешние источники), вам следует проверить это модуль из:

https://github.com/fresheneesz/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')
    }
  })
})

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

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

Альтернативное рабочее решение, немного проще.

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

Это не всегда работает в Chrome. Иногда я получаю clientXвыше 0, когда мышь находится за пределами поля. Конечно, мои элементыposition:absolute
— Hengjie

Это происходит постоянно или только иногда? Потому что, если мышь движется слишком быстро (например, за пределами окна), вы можете получить неправильные значения.
— Profet

Это происходит в 90% случаев. Есть редкие случаи (1 из 10 раз), когда я могу достигнуть 0. Я попробую еще раз двигать мышь медленнее, но я не могу сказать, что я двигался быстро (возможно, то, что вы бы назвали нормальной скоростью).
— Hengjie

2

Потратив так много часов, я понял, что это предложение работает точно так, как задумано. Я хотел предоставить сигнал только тогда, когда файлы перетаскивались, а при перетаскивании документа, dragleave вызывал болезненные мерцания в браузере 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
});

1

У меня была похожая проблема - мой код для скрытия dropzone на событии dragleave для body был запущен одновременно, когда парящие дочерние элементы вызывали мерцание dropzone в Google Chrome.

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

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

Это то, что я тоже делал, но все еще отрывочно.
— mpen

1

« DragLeave » событие вызывается , когда указатель мыши покидает зону перетаскивания целевого контейнера.

Это имеет большой смысл, так как во многих случаях может сбрасываться только родитель, а не потомки. Я думаю, что event.stopPropogation () должен был обработать этот случай, но кажется, что он не работает.

Вышеупомянутые решения, кажется, работают в большинстве случаев, но не работают в случае тех дочерних элементов, которые не поддерживают события dragenter / dragleave , такие как iframe .

1 обходной путь - проверить event.relatedTarget и убедиться, что он находится внутри контейнера, а затем проигнорировать событие dragleave, как я это сделал здесь:

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

Вы можете найти рабочую скрипку здесь !


1

Решено ..!

Объявите любой массив для 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');
}

Не нужно беспокоиться о дочерних элементах.


1

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

В моем примере ниже у меня есть таблица, где я хочу иметь возможность обмениваться содержимым строк таблицы друг с другом с помощью API перетаскивания. В dragenter, класс CSS должен быть добавлен к строке элемента , в котором вы сейчас тащит свой элемент, чтобы выделить его, и на dragleaveэтот класс должен быть удален.

Пример:

Очень простая таблица HTML:

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

А функция DragEnter обработчик события добавляется на каждую ячейку таблицы (кроме dragstart, dragover, dropи dragendобработчики, которые не являются специфическими для этого вопроса, поэтому не скопированные здесь):

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

В коде остались очень простые комментарии, так что этот код подходит и для начинающих. Надеюсь, это поможет вам! Обратите внимание, что вам, конечно, нужно добавить всех прослушивателей событий, о которых я упоминал выше, в каждую ячейку таблицы, чтобы это работало.


0

Вот еще один подход, основанный на времени событий.

dragenterСобытие отправляется из дочернего элемента может быть захвачено родительским элементом , и это всегда происходит перед dragleave. Время между этими двумя событиями действительно короткое, короче, чем любое возможное действие мыши. Таким образом, идея состоит в том, чтобы запомнить время, когда dragenterпроисходит, и отфильтровать dragleaveсобытия, которые происходят "не слишком быстро" после ...

Этот короткий пример работает на Chrome и 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 ..

Почему бы вам не попробовать использовать drop вместо dragleave . Это сработало для меня. надеюсь, что это решит вашу проблему.

Пожалуйста, проверьте 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
Если пользователь передумает и перетянет файл обратно из браузера, он останется красным.
— ZachB

0

Вы можете использовать тайм-аут с transitioningфлагом и прослушивать верхний элемент. dragenter/ dragleaveот дочерних событий будет всплывать до контейнера.

Так dragenterкак дочерний элемент запускается перед dragleaveконтейнером, мы установим флаг show как переходный на 1 мс ... dragleaveслушатель проверит флаг на наличие 1 мс.

Флаг будет истинным только при переходах к дочерним элементам и не будет истинным при переходе к родительскому элементу (контейнера)

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

Вам необходимо удалить события указателя для всех дочерних объектов цели перетаскивания.

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) {
                //
            }
        }
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.