Вот еще одно решение.Я написал это в 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)
, и проще, чем многие другие хаки, которые я видел.
Тогда другой «трюк», который я делаю, - это очистить / отменить обратный вызов события exit, когда вы удаляете файл только вв случае, если у нас был ожидающий ответный вызов - это предотвратит переход счетчика в негативы и все испортит.
Вот и все.Кажется, работает очень хорошо в моем первоначальном тестировании.Без задержек, без мигания моей цели сброса.
Можно также получить количество файлов с помощью ev.dataTransfer.items.length