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.