如何像Gmail一样检测进入和离开窗口的HTML5拖动事件?

问题描述 投票:22回答:10

我希望能够在携带文件的光标进入浏览器窗口后立即突出显示丢弃区域,就像Gmail一样。但我不能让它发挥作用,我觉得我只是错过了一些非常明显的东西。

我一直在努力做这样的事情:

this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`

但是,每当光标移过和移出除BODY之外的元素时,它都会触发事件,这是有道理的,但绝对不起作用。我可以在所有东西上面放置一个元素,覆盖整个窗口并检测到它,但这是一种可怕的方式。

我错过了什么?

html5 drag-and-drop gmail
10个回答
23
投票

我用超时解决了它(不是吱吱作响,但是有效):

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和qtml(或body)元素的true上设置一个标志(dragenter)到dragover
  • dragleave上设置了false的旗帜。然后设置一个简短的超时以检查该标志是否仍为false。
  • 理想情况下,跟踪超时并在设置下一个超时之前将其清除。

这样,每个dragleave事件都为DOM提供了足够的时间来重新启动新的dragover事件。我们关心的真实的,最终的dragleave将看到旗帜仍然是假的。


0
投票

这是另一种解决方案。我在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将回调移动到下一个tick。这比setImmediate(..., 0)更快,比我见过的许多其他黑客更简单。

然后我做的另一个“技巧”就是在你放弃一个文件时清除/取消离开事件回调以防万一我们有一个挂起的回调 - 这将阻止计数器进入底片并搞乱一切。

而已。似乎在我的初始测试中工作得很好。没有延迟,我的掉落目标没有闪烁。


也可以使用ev.dataTransfer.items.length获取文件数


8
投票

不知道它适用于所有情况,但在我的情况下它工作得很好

$('body').bind("dragleave", function(e) {
   if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
      //outside body / window
   }
});

6
投票

将事件添加到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);

3
投票

@ 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
});

1
投票

你对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不是身体,所以我们不喜欢这样并返回。如果thisbody,则该集将为空并且代码将执行。

你可以尝试一个简单的if (this == $('body').get(0)),但这可能会失败。


1
投票

我自己遇到了麻烦,想出了一个可用的解决方案,尽管我不必为使用叠加层而疯狂。

ondragoverondragleaveondrop添加到窗口

ondragenterondragleaveondrop添加到叠加层和目标元素

如果窗口或叠加层上出现掉落,则会忽略它,而目标会根据需要处理掉落。我们需要叠加的原因是因为每次元素悬停时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>

0
投票

您是否注意到在Gmail中删除区域消失之前有一段延迟?我的猜测是,它们会在计时器(约500毫秒)上消失,这个计时器会被dragover或某些此类事件重置。

您描述的问题的核心是即使拖动到子元素中也会触发dragleave。我正试图找到一种方法来检测这一点,但我还没有一个优雅清洁的解决方案。


0
投票

非常抱歉发布有角度和下划线的内容,但是我解决问题的方式(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实现。 (拖放)。

真的希望这有助于一些人。


0
投票

当文件进入并离开子元素时,它会触发额外的dragenterdragleave,因此您需要向上和向下计数。

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
})
© www.soinside.com 2019 - 2024. All rights reserved.