如何像 Gmail 一样检测进入和离开窗口的 HTML5 拖动事件?
Posted
技术标签:
【中文标题】如何像 Gmail 一样检测进入和离开窗口的 HTML5 拖动事件?【英文标题】:How do I detect a HTML5 drag event entering and leaving the window, like Gmail does? 【发布时间】:2011-03-09 20:41:41 【问题描述】:我希望能够在带有文件的光标进入浏览器窗口时突出显示放置区域,这与 Gmail 的做法完全一样。但我无法让它发挥作用,我觉得我只是错过了一些非常明显的东西。
我一直在尝试做这样的事情:
this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
但是,只要光标移出和移出 BODY 以外的元素,就会触发事件,这是有道理的,但绝对行不通。我可以在所有东西上放置一个元素,覆盖整个窗口并对其进行检测,但这将是一种可怕的方式。
我错过了什么?
【问题讨论】:
除了下面的答案:我注意到至少在 chrome 下,事件的顺序是: ENTER ENTER LEAVE ENTER LEAVE ... LEAVE 这意味着如果你保持进入和离开的数量,你将能够区分初始进入和内部进入/离开序列 PS:抱歉,格式化受够了...... 你就是那个人@MartinWawrusch!谢谢你 @MartinWawrusch 几乎完美。如果页面本身内部发生“丢弃”,则不会触发最后一次离开。因此,为了完整起见,ENTER [ENTER LEAVE]* [LEAVE|DROP]
是正确的顺序。应该使用捕获(将第三个参数设置为true
)而不是在 html/document/body 元素上注册事件在window
上,这样如果其他人在某事上调用e.stopPropagation()
,则不会错过任何事件在树的更深处。
【参考方案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,但这不是必需的。以下是正在发生的事情的摘要:
在 html(或正文)元素的dragenter
和 dragover
上将标志 (showDrag
) 设置为 true
。
在dragleave
上将标志设置为false
。然后设置一个短暂的超时来检查标志是否仍然为假。
理想情况下,在设置下一个超时之前跟踪并清除它。
这样,每个dragleave
事件都会给DOM 足够的时间让新的dragover
事件重置标志。我们关心的真实的、最终的 dragleave
会看到标志仍然是假的。
【讨论】:
将此答案与 Martin Wawrusch 对该问题的评论相结合,我得到了最好的结果。每次处理任何事件时都会重置计时器,并在拖动离开相等的拖动者时完全取消。这给了我在 webkit 浏览器中的即时反馈,并给了我在 firefox 中短暂滞后的反馈,它在离开窗口时不能可靠地调用 dragleave。 换句话说,您去抖动dragleave
事件的处理程序。聪明的解决方法。【参考方案2】:
Rehmat 的修改版(谢谢)
我喜欢这个想法,而不是写一个新的答案,我在这里自己更新它。通过检查窗口尺寸可以使其更精确。
var body = document.querySelector("body");
body.ondragleave = (e) =>
if (
e.clientX >= 0 && e.clientX <= body.clientWidth
&& e.clientY >= 0 && e.clientY <= body.clientHeight
) else
// do something here
旧版本
不知道这对所有情况都有效,但在我的情况下效果很好
$('body').bind("dragleave", function(e)
if (!e.originalEvent.clientX && !e.originalEvent.clientY)
//outside body / window
);
【讨论】:
这并不总能成功检测到拖动的结束(例如:从浏览器外部拖动并放在 chrome 中的“下载”栏上的文件不会发送带有 0,0 作为客户X/Y 这可以向上、向下和向左工作。如果有滚动条,右键会失败。【参考方案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】:这是另一个解决方案。我是用 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
将事件推送到 javascript 事件队列的底部。
setImmediate
没有得到很好的支持,所以我编写了自己的版本,无论如何我更喜欢它。我还没有看到其他人像这样实现它。我使用Promise.resolve().then
将回调移动到下一个刻度。这比 setImmediate(..., 0)
更快,并且比我见过的许多其他 hack 更简单。
然后我做的另一个“技巧”是在您删除文件时清除/取消离开事件回调,以防万一我们有一个待处理的回调——这将防止计数器进入负数并搞砸一切。
就是这样。在我的初始测试中似乎工作得很好。没有延迟,我的放置目标没有闪烁。
ev.dataTransfer.items.length
也可以获取文件数
【讨论】:
这是一个很好的答案,效果很好。我已经移植了用作 Vue 指令的代码:gist.github.com/Radiergummi/5b6f9a3c59b3c11130065443c77b4f0f【参考方案6】: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))
,但这可能会失败。
【讨论】:
【参考方案7】:我自己遇到了这个问题,并想出了一个可用的解决方案,尽管我对必须使用叠加层并不感到疯狂。
将ondragover
、ondragleave
和ondrop
添加到窗口
将ondragenter
、ondragleave
和ondrop
添加到叠加层和目标元素
如果拖放发生在窗口或覆盖上,它会被忽略,而目标会根据需要处理拖放。我们需要叠加层的原因是因为ondragleave
会在每次元素悬停时触发,因此叠加层可以防止这种情况发生,而放置区被赋予更高的 z-index 以便可以放置文件。我正在使用在其他拖放相关问题中找到的一些代码 sn-ps,所以我不能完全相信。这是完整的 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>
【讨论】:
【参考方案8】:我看到很多过度设计的解决方案。听听dragenter
和dragleave
,你应该能够做到这一点,就像你的直觉告诉你的那样。
棘手的部分是,当dragleave
触发时,它的toElement
和fromElement
似乎与日常生活中的意义相反(这在逻辑上是有意义的,因为它是@ 的倒置动作987654326@)。
当您将光标从监听元素移动到该元素之外时,toElement
将具有监听元素,fromElement
将具有外部非监听元素。在我们的例子中,当我们拖出浏览器时,fromElement 将是null
。
解决方案
window.addEventListener("dragleave", function(e)
if (!e.fromElement)
console.log("Dragging back to OS")
)
window.addEventListener("dragenter", function(e)
console.log("Dragging to browser")
)
【讨论】:
【参考方案9】:ondragenter 经常被解雇。您可以避免使用像 draggedFile
这样的辅助变量。如果您不关心 on ondragenter
函数被调用的频率,您可以删除该辅助变量。
解决方案:
let draggedFile = false;
window.ondragenter = (e) =>
if(!draggedFile)
draggedFile = true;
console.log("dragenter");
window.ondragleave = (e) =>
if (!e.fromElement && draggedFile)
draggedFile = false;
console.log("dragleave");
【讨论】:
【参考方案10】:您是否注意到 Gmail 中的拖放区会出现延迟?我的猜测是他们让它在一个计时器(约 500 毫秒)上消失,该计时器通过拖动或某些此类事件重置。
您描述的问题的核心是即使您拖动到子元素中也会触发dragleave。我正在尝试找到一种方法来检测这一点,但我还没有一个优雅干净的解决方案。
【讨论】:
【参考方案11】:很抱歉发布特定于角度和下划线的内容,但是我解决问题的方式(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);
);
;
)
我使用一个列表来表示触发了拖入事件的元素。当拖动离开事件发生时,我在拖动输入列表中找到匹配的元素,将其从列表中删除,如果结果列表为空,我知道我已拖动到文档/窗口之外。
我需要在放置事件发生后重置包含拖动元素的列表,或者下次我开始拖动某些内容时,列表将填充上次拖放操作中的元素。
到目前为止,我只在 chrome 上对此进行了测试。我这样做是因为 Firefox 和 chrome 有不同的 HTML5 DND API 实现。 (拖放)。
真的希望这对某些人有所帮助。
【讨论】:
【参考方案12】:当文件进入和离开子元素时,它会触发额外的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
)
【讨论】:
【参考方案13】:我通过查看spec 发现,如果dragEnd 上的evt.dataTransfer.dropEffect
匹配none
,那么它就是取消。
我确实已经使用该事件来处理复制而不影响剪贴板。所以这对我有好处。
当我点击 Esc 然后下降效果等于none
window.ondragend = evt =>
if (evt.dataTransfer.dropEffect === 'none') abort
if (evt.dataTransfer.dropEffect === 'copy') copy // user holds alt on mac
if (evt.dataTransfer.dropEffect === 'move') move
【讨论】:
【参考方案14】:在“dropend”事件中,您可以检查 document.focus() 的值,这对我来说是魔术。
【讨论】:
这应该是一条评论以上是关于如何像 Gmail 一样检测进入和离开窗口的 HTML5 拖动事件?的主要内容,如果未能解决你的问题,请参考以下文章
如果通知中心像 Dropbox 和 1Password 一样简单地显示,我如何检测用户解锁设备但不检测?