Html 如何像 Gmail 一样检测进入和离开窗口的 HTML5 拖动事件?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3144881/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do I detect a HTML5 drag event entering and leaving the window, like Gmail does?
提问by Heilemann
I'd like to be able to highlight the drop area as soon as the cursor carrying a file enters the browser window, exactly the way Gmail does it. But I can't make it work, and I feel like I'm just missing something really obvious.
我希望能够在带有文件的光标进入浏览器窗口时立即突出显示拖放区域,这与 Gmail 的做法完全一样。但我无法让它发挥作用,我觉得我只是错过了一些非常明显的东西。
I keep trying to do something like this:
我一直在尝试做这样的事情:
this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
But that fires the events whenever the cursor moves over and out of elements other than BODY, which makes sense, but absolutely doesn't work. I could place an element on top of everything, covering the entire window and detect on that, but that'd be a horrible way to go about it.
但是,只要光标移出 BODY 以外的元素,就会触发事件,这是有道理的,但绝对不起作用。我可以在所有东西的顶部放置一个元素,覆盖整个窗口并对其进行检测,但这将是一种可怕的方法。
What am I missing?
我错过了什么?
回答by Tyler
I solved it with a timeout (not squeaky-clean, but works):
我用超时解决了它(不是吱吱作响,但有效):
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 );
});
My example uses jQuery, but it's not necessary. Here's a summary of what's going on:
我的示例使用 jQuery,但这不是必需的。以下是正在发生的事情的摘要:
- Set a flag (
showDrag
) totrue
ondragenter
anddragover
of the html (or body) element. - On
dragleave
set the flag tofalse
. Then set a brief timeout to check if the flag is still false. - Ideally, keep track of the timeout and clear it before setting the next one.
- 将标志 (
showDrag
)设置为html(或 body)元素的true
ondragenter
和dragover
。 - 在
dragleave
将标志设置为false
. 然后设置一个短暂的超时来检查标志是否仍然为假。 - 理想情况下,跟踪超时并在设置下一个之前清除它。
This way, each dragleave
event gives the DOM enough time for a new dragover
event to reset the flag. The real, finaldragleave
that we care about will see that the flag is still false.
这样,每个dragleave
事件都为 DOM 提供了足够的时间让新dragover
事件重置标志。我们关心的真实的 finaldragleave
将看到 flag 仍然是 false。
回答by cinatic
Don't know it this works for all cases but in my case it worked very well
不知道这适用于所有情况,但在我的情况下它工作得很好
$('body').bind("dragleave", function(e) {
if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
//outside body / window
}
});
回答by ile
Adding the events to document
seemed to work? Tested with Chrome, Firefox, IE 10.
添加事件document
似乎有效?使用 Chrome、Firefox、IE 10 进行测试。
The first element that gets the event is <html>
, which should be ok I think.
获取事件的第一个元素是<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);
回答by visitsb
@tyler's answer is the best! I have upvoted it. After spending so many hours I got that suggestion working exactly as intended.
@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
});
回答by MvanGeest
Your third argument to addEventListener
is true
, which makes the listener run during capture phase (see http://www.w3.org/TR/DOM-Level-3-Events/#event-flowfor a visualization). This means that it will capture the events intended for its descendants - and for the body that means all elements on the page. In your handlers, you'll have to check if the element they're triggered for is the body itself. I'll give you my very dirty way of doing it. If anyone knows a simpler way that actually compares elements, I'd love to see it.
您的第三个参数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 finds the body and removes this
from the set of elements found. If the set isn't empty, this
wasn't the body, so we don't like this and return. If this
is body
, the set will be empty and the code executes.
这将找到主体并this
从找到的元素集中删除。如果 set 不是空的,this
则不是 body,所以我们不喜欢这个并返回。如果this
是body
,则集合将为空并且代码执行。
You can try with a simple if (this == $('body').get(0))
, but that will probably fail miserably.
您可以尝试使用简单的if (this == $('body').get(0))
,但这可能会失败。
回答by Mathachew
I was having trouble with this myself and came up with a usable solution, though I'm not crazy about having to use an overlay.
我自己也遇到了这个问题,并提出了一个可用的解决方案,尽管我对必须使用叠加层并不感到疯狂。
Add ondragover
, ondragleave
and ondrop
to window
添加ondragover
,ondragleave
和ondrop
到窗口
Add ondragenter
, ondragleave
and ondrop
to an overlay and a target element
将ondragenter
,ondragleave
和添加ondrop
到叠加层和目标元素
If drop occurs on the window or overlay, it is ignored, whereas the target handles the drop as desired. The reason we need an overlay is because ondragleave
triggers every time an element is hovered, so the overlay prevents that from happening, while the drop zone is given a higher z-index so that the files can be dropped. I am using some code snippets found in other drag and drop related questions, so I cannot take full credit. Here's the full HTML:
如果在窗口或覆盖层上发生放置,则将其忽略,而目标会根据需要处理放置。我们需要叠加层的原因是因为ondragleave
每次悬停元素时都会触发,因此叠加层可以防止这种情况发生,而拖放区被赋予更高的 z-index,以便可以删除文件。我正在使用在其他拖放相关问题中找到的一些代码片段,所以我不能完全相信。这是完整的 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>
回答by dlo
Have you noticed that there is a delay before the dropzone disappears in Gmail? My guess is that they have it disappear on a timer (~500ms) that gets reset by dragover or some such event.
您是否注意到 Gmail 中 dropzone 消失之前有一段延迟?我的猜测是,他们让它在计时器(~500 毫秒)上消失,该计时器通过拖动或某些此类事件重置。
The core of the problem you described is that dragleave is triggered even when you drag into a child element. I'm trying to find a way to detect this, but I don't have an elegantly clean solution yet.
您描述的问题的核心是,即使您拖入子元素,也会触发dragleave。我试图找到一种方法来检测这一点,但我还没有一个优雅干净的解决方案。
回答by PPPaul
really sorry to post something that is angular & underscore specific, however the way i solved the problem (HTML5 spec, works on chrome) should be easy to observe.
真的很抱歉发布特定于角度和下划线的内容,但是我解决问题的方式(HTML5 规范,适用于 chrome)应该很容易观察。
.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);
});
}
};
})
})
I use a list to represent the elements that have triggered a drag enter event. when a drag leave event happens i find the element in the drag enter list that matches, remove it from the list, and if the resulting list is empty i know that i have dragged outside of the document/window.
我使用一个列表来表示触发了拖动输入事件的元素。当拖动离开事件发生时,我在拖动输入列表中找到匹配的元素,将其从列表中删除,如果结果列表为空,我知道我已拖动到文档/窗口之外。
I need to reset the list containing dragged over elements after a drop event occurs, or the next time I start dragging something the list will be populated with elements from the last drag and drop action.
我需要在发生放置事件后重置包含拖拽元素的列表,或者下次我开始拖动某些内容时,列表将填充上次拖放操作中的元素。
I have only tested this on chrome so far. I made this because Firefox and chrome have different API implementations of HTML5 DND. (drag and drop).
到目前为止,我只在 chrome 上测试过这个。我这样做是因为 Firefox 和 chrome 有不同的 HTML5 DND API 实现。(拖放)。
really hope this helps some people.
真的希望这可以帮助一些人。
回答by AJcodez
When the file enters and leaves child elements it fires additional dragenter
and dragleave
so you need to count up and down.
当文件进入和离开子元素时,它会额外触发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
})
回答by mpen
Here's another solution. I wrote it in React, but I'll explain it at the end if you want to rebuild it in plain JS. It's similar to other answers here, but perhaps slightly more refined.
这是另一个解决方案。我是用 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;
}
}
So, as others have observed, the dragleave
event fires before the next dragenter
fires, which means our counter will momentarily hit 0 as we drag files (or whatever) around the page. To prevent that, I've used setImmediate
to push the event to the bottom of JavaScript's event queue.
因此,正如其他人所观察到的,该dragleave
事件在下一次触发之前dragenter
触发,这意味着当我们在页面上拖动文件(或其他任何内容)时,我们的计数器将暂时达到 0。为了防止这种情况,我习惯于setImmediate
将事件推送到 JavaScript 事件队列的底部。
setImmediate
isn't well supported, so I wrote my own version which I like better anyway. I haven't seen anyone else implement it quite like this. I use Promise.resolve().then
to move the callback to the next tick. This is faster than setImmediate(..., 0)
and simpler than many of the other hacks I've seen.
setImmediate
没有得到很好的支持,所以我写了我自己的版本,我更喜欢它。我还没有看到其他人像这样实现它。我Promise.resolve().then
用来将回调移动到下一个刻度。这比setImmediate(..., 0)
我见过的许多其他黑客攻击更快也更简单。
Then the other "trick" I do is to clear/cancel the leave event callback when you drop a file just in case we had a callback pending -- this will prevent the counter from going into the negatives and messing everything up.
然后我做的另一个“技巧”是在您删除文件时清除/取消离开事件回调,以防万一我们有一个待处理的回调——这将防止计数器进入底片并将一切搞砸。
That's it. Seems to work very well in my initial testing. No delays, no flashing of my drop target.
就是这样。在我的初始测试中似乎工作得很好。没有延迟,没有闪烁我的下降目标。
Can get the file count too with ev.dataTransfer.items.length
也可以获取文件计数 ev.dataTransfer.items.length