这是另一个解决方案。我是用 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也可以获取文件数