Как определить событие перетаскивания HTML5, входящее и выходящее из окна, например Gmail?
Я хочу, чтобы можно было выделить область выделения, как только курсор, переносящий файл, войдет в окно браузера, точно так же, как это делает Gmail. Но я не могу заставить его работать, и мне кажется, что я просто пропущу что-то действительно очевидное.
Я продолжаю пытаться сделать что-то вроде этого:
this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
Но это вызывает события, когда курсор перемещается и выходит за пределы элементов, отличных от BODY, что имеет смысл, но абсолютно не работает. Я мог бы разместить элемент поверх всего, покрывая все окно и обнаруживая это, но это был бы ужасный способ сделать это.
Что мне не хватает?
Ответы
Ответ 1
Я решил это с тайм-аутом (не скрипучим-чистым, но работает):
var dropTarget = $('.dropTarget'),
html = $('html'),
showDrag = false,
timeout = -1;
html.bind('dragenter', function () {
dropTarget.addClass('dragging');
showDrag = true;
});
html.bind('dragover', function(){
showDrag = true;
});
html.bind('dragleave', function (e) {
showDrag = false;
clearTimeout( timeout );
timeout = setTimeout( function(){
if( !showDrag ){ dropTarget.removeClass('dragging'); }
}, 200 );
});
В моем примере используется jQuery, но это необязательно. Вот краткое описание того, что происходит:
- Установите флаг (
showDrag
) на true
на dragenter
и dragover
элемента html (или body).
- В
dragleave
установите флаг false
. Затем установите короткий тайм-аут, чтобы проверить, остается ли флаг false.
- В идеале, отслеживайте таймаут и очищайте его перед установкой следующего.
Таким образом, каждое событие dragleave
дает DOM достаточное время для нового события dragover
для reset флага. Реальный, окончательный dragleave
, который нас волнует, увидит, что флаг по-прежнему остается ложным.
Ответ 2
Не знаю, это работает для всех случаев, но в моем случае это работало очень хорошо
$('body').bind("dragleave", function(e) {
if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
//outside body / window
}
});
Ответ 3
Возможно, добавили события в document
? Протестировано с помощью Chrome, Firefox, IE 10.
Первый элемент, который получает событие, <html>
, который должен быть в порядке, я думаю.
var dragCount = 0,
dropzone = document.getElementById('dropzone');
function dragenterDragleave(e) {
e.preventDefault();
dragCount += (e.type === "dragenter" ? 1 : -1);
if (dragCount === 1) {
dropzone.classList.add('drag-highlight');
} else if (dragCount === 0) {
dropzone.classList.remove('drag-highlight');
}
};
document.addEventListener("dragenter", dragenterDragleave);
document.addEventListener("dragleave", dragenterDragleave);
Ответ 4
@tyler ответ лучший! Я поддержал это. Проведя так много часов, я получил это предложение, работающее точно так, как предполагалось.
$(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
});
Ответ 5
Ваш третий аргумент addEventListener
- true
, что заставляет слушателя работать во время фазы захвата (см. http://www.w3.org/TR/DOM-Level-3-Events/#event-flow для визуализация). Это означает, что он будет захватывать события, предназначенные для его потомков, а для тела - все элементы на странице. В ваших обработчиках вам нужно будет проверить, является ли элемент, для которого они запускаются, является самим телом. Я дам вам очень грязный способ сделать это. Если кто-нибудь знает более простой способ , который на самом деле сравнивает элементы, мне бы очень хотелось его увидеть.
this.dragenter = function() {
if ($('body').not(this).length != 0) return;
... functional code ...
}
Это находит тело и удаляет this
из набора найденных элементов. Если набор не пуст, this
не был телом, поэтому нам это не нравится и возвращается. Если this
- body
, набор будет пустым и код будет выполнен.
Вы можете попробовать с помощью простого if (this == $('body').get(0))
, но это, вероятно, потерпит неудачу.
Ответ 6
У меня возникли проблемы с этим самим и придумал полезное решение, хотя я не сумасшедший, чтобы использовать оверлей.
Добавьте ondragover
, ondragleave
и ondrop
в окно
Добавьте ondragenter
, ondragleave
и ondrop
к наложению и целевому элементу
Если в окне или наложении происходит падение, оно игнорируется, тогда как цель обрабатывает падение по желанию. Причина, по которой нам требуется оверлей, состоит в том, что ondragleave
запускается каждый раз, когда элемент зависает, поэтому наложение предотвращает это, тогда как в зоне выпадения указан более высокий индекс z, чтобы файлы можно было отбросить. Я использую некоторые фрагменты кода, найденные в других вопросах, связанных с перетаскиванием, поэтому я не могу получить полный кредит. Здесь полный HTML:
<!DOCTYPE html>
<html>
<head>
<title>Drag and Drop Test</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<style>
#overlay {
display: none;
left: 0;
position: absolute;
top: 0;
z-index: 100;
}
#drop-zone {
background-color: #e0e9f1;
display: none;
font-size: 2em;
padding: 10px 0;
position: relative;
text-align: center;
z-index: 150;
}
#drop-zone.hover {
background-color: #b1c9dd;
}
output {
bottom: 10px;
left: 10px;
position: absolute;
}
</style>
<script>
var windowInitialized = false;
var overlayInitialized = false;
var dropZoneInitialized = false;
function handleFileSelect(e) {
e.preventDefault();
var files = e.dataTransfer.files;
var output = [];
for (var i = 0; i < files.length; i++) {
output.push('<li>',
'<strong>', escape(files[i].name), '</strong> (', files[i].type || 'n/a', ') - ',
files[i].size, ' bytes, last modified: ',
files[i].lastModifiedDate ? files[i].lastModifiedDate.toLocaleDateString() : 'n/a',
'</li>');
}
document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
}
window.onload = function () {
var overlay = document.getElementById('overlay');
var dropZone = document.getElementById('drop-zone');
dropZone.ondragenter = function () {
dropZoneInitialized = true;
dropZone.className = 'hover';
};
dropZone.ondragleave = function () {
dropZoneInitialized = false;
dropZone.className = '';
};
dropZone.ondrop = function (e) {
handleFileSelect(e);
dropZoneInitialized = false;
dropZone.className = '';
};
overlay.style.width = (window.innerWidth || document.body.clientWidth) + 'px';
overlay.style.height = (window.innerHeight || document.body.clientHeight) + 'px';
overlay.ondragenter = function () {
if (overlayInitialized) {
return;
}
overlayInitialized = true;
};
overlay.ondragleave = function () {
if (!dropZoneInitialized) {
dropZone.style.display = 'none';
}
overlayInitialized = false;
};
overlay.ondrop = function (e) {
e.preventDefault();
dropZone.style.display = 'none';
};
window.ondragover = function (e) {
e.preventDefault();
if (windowInitialized) {
return;
}
windowInitialized = true;
overlay.style.display = 'block';
dropZone.style.display = 'block';
};
window.ondragleave = function () {
if (!overlayInitialized && !dropZoneInitialized) {
windowInitialized = false;
overlay.style.display = 'none';
dropZone.style.display = 'none';
}
};
window.ondrop = function (e) {
e.preventDefault();
windowInitialized = false;
overlayInitialized = false;
dropZoneInitialized = false;
overlay.style.display = 'none';
dropZone.style.display = 'none';
};
};
</script>
</head>
<body>
<div id="overlay"></div>
<div id="drop-zone">Drop files here</div>
<output id="list"><output>
</body>
</html>
Ответ 7
Вы заметили, что существует задержка до того, как dropzone исчезнет в Gmail? Я предполагаю, что они исчезают на таймере (~ 500 мс), который получает reset с помощью dragover или какого-то такого события.
Ядро проблемы, которую вы описали, заключается в том, что dragleave запускается даже при перетаскивании в дочерний элемент. Я пытаюсь найти способ обнаружить это, но пока у меня нет элегантно чистого решения.
Ответ 8
действительно жаль опубликовать что-то, что есть angular, и подчеркнуть специфику, однако, как я решил проблему (спецификация HTML5, работает на хроме), должно быть легко наблюдать.
.directive('documentDragAndDropTrigger', function(){
return{
controller: function($scope, $document){
$scope.drag_and_drop = {};
function set_document_drag_state(state){
$scope.$apply(function(){
if(state){
$document.context.body.classList.add("drag-over");
$scope.drag_and_drop.external_dragging = true;
}
else{
$document.context.body.classList.remove("drag-over");
$scope.drag_and_drop.external_dragging = false;
}
});
}
var drag_enters = [];
function reset_drag(){
drag_enters = [];
set_document_drag_state(false);
}
function drag_enters_push(event){
var element = event.target;
drag_enters.push(element);
set_document_drag_state(true);
}
function drag_leaves_push(event){
var element = event.target;
var position_in_drag_enter = _.find(drag_enters, _.partial(_.isEqual, element));
if(!_.isUndefined(position_in_drag_enter)){
drag_enters.splice(position_in_drag_enter,1);
}
if(_.isEmpty(drag_enters)){
set_document_drag_state(false);
}
}
$document.bind("dragenter",function(event){
console.log("enter", "doc","drag", event);
drag_enters_push(event);
});
$document.bind("dragleave",function(event){
console.log("leave", "doc", "drag", event);
drag_leaves_push(event);
console.log(drag_enters.length);
});
$document.bind("drop",function(event){
reset_drag();
console.log("drop","doc", "drag",event);
});
}
};
})
Я использую список для представления элементов, вызвавших событие перетаскивания. когда происходит событие перетаскивания, я нахожу элемент в списке перетаскивания, который соответствует, удаляет его из списка, и если результирующий список пуст, я знаю, что я перетащил его за пределы документа/окна.
Мне нужно reset список, содержащий перетаскиваемые элементы после события пересылки, или в следующий раз, когда я начну перетаскивать что-то, список будет заполнен элементами из последнего действия перетаскивания.
Я тестировал это только на хроме. Я сделал это, потому что Firefox и хром имеют разные версии API HTML5 DND. (перетаскивание).
действительно надеюсь, что это поможет некоторым людям.
Ответ 9
Когда файл вводит и оставляет дочерние элементы, он запускает дополнительные dragenter
и dragleave
, поэтому вам нужно подсчитывать вверх и вниз.
var count = 0
document.addEventListener("dragenter", function() {
if (count === 0) {
setActive()
}
count++
})
document.addEventListener("dragleave", function() {
count--
if (count === 0) {
setInactive()
}
})
document.addEventListener("drop", function() {
if (count > 0) {
setInactive()
}
count = 0
})
Ответ 10
Здесь другое решение. Я написал это в React, но я объясню это в конце, если вы хотите перестроить его на обычном JS. Это похоже на другие ответы здесь, но, возможно, немного более изысканным.
import React from 'react';
import styled from '@emotion/styled';
import BodyEnd from "./BodyEnd";
const DropTarget = styled.div'
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
background-color:rgba(0,0,0,.5);
';
function addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions) {
document.addEventListener(type, listener, options);
return () => document.removeEventListener(type, listener, options);
}
function setImmediate(callback: (...args: any[]) => void, ...args: any[]) {
let cancelled = false;
Promise.resolve().then(() => cancelled || callback(...args));
return () => {
cancelled = true;
};
}
function noop(){}
function handleDragOver(ev: DragEvent) {
ev.preventDefault();
ev.dataTransfer!.dropEffect = 'copy';
}
export default class FileDrop extends React.Component {
private listeners: Array<() => void> = [];
state = {
dragging: false,
}
componentDidMount(): void {
let count = 0;
let cancelImmediate = noop;
this.listeners = [
addEventListener('dragover',handleDragOver),
addEventListener('dragenter',ev => {
ev.preventDefault();
if(count === 0) {
this.setState({dragging: true})
}
++count;
}),
addEventListener('dragleave',ev => {
ev.preventDefault();
cancelImmediate = setImmediate(() => {
--count;
if(count === 0) {
this.setState({dragging: false})
}
})
}),
addEventListener('drop',ev => {
ev.preventDefault();
cancelImmediate();
if(count > 0) {
count = 0;
this.setState({dragging: false})
}
}),
]
}
componentWillUnmount(): void {
this.listeners.forEach(f => f());
}
render() {
return this.state.dragging ? <BodyEnd><DropTarget/></BodyEnd> : null;
}
}
Итак, как уже отмечали другие, событие dragleave
запускается до следующего срабатывания dragenter
, что означает, что наш счетчик мгновенно достигнет 0, когда мы будем перетаскивать файлы (или что-то еще) по странице. Чтобы предотвратить это, я использовал setImmediate
чтобы setImmediate
событие в setImmediate
очереди событий JavaScript.
setImmediate
не очень хорошо поддерживается, поэтому я написал свою собственную версию, которая мне все равно нравится больше. Я не видел, чтобы кто-нибудь еще реализовал это так. Promise.resolve().then
я использую Promise.resolve().then
Чтобы переместить обратный вызов на следующий тик. Это быстрее, чем setImmediate(..., 0)
и проще, чем многие другие хаки, которые я видел.
Затем другой "трюк", который я делаю, - это очистить/отменить обратный вызов события покидания, когда вы удаляете файл на случай, если у нас был отложенный обратный вызов - это предотвратит переход счетчика в негативы и все испортит.
Это. Кажется, работает очень хорошо в моем первоначальном тестировании. Никаких задержек, никаких всплесков моей цели.
Может получить количество файлов тоже с ev.dataTransfer.items.length
![]()