【问题标题】:How do I detect a HTML5 drag event entering and leaving the window, like Gmail does?如何像 Gmail 一样检测进入和离开窗口的 HTML5 拖动事件?
【发布时间】: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(),则不会错过任何事件在树的更深处。

标签: html drag-and-drop gmail


【解决方案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(或正文)元素的 dragenterdragover 上将标志 (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 测试。

获得事件的第一个元素是&lt;html&gt;,我认为应该没问题。

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也可以获取文件数

      【讨论】:

      【解决方案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 就不是主体,所以我们不喜欢这个并返回。如果thisbody,则集合为空,代码执行。

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

      【讨论】:

        【解决方案7】:

        我自己遇到了这个问题,并想出了一个可用的解决方案,尽管我对必须使用叠加层并不感到疯狂。

        ondragoverondragleaveondrop添加到窗口

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

        如果拖放发生在窗口或覆盖上,它会被忽略,而目标会根据需要处理拖放。我们需要叠加层的原因是因为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】:

          我看到很多过度设计的解决方案。听听dragenterdragleave,你应该能够做到这一点,就像你的直觉告诉你的那样。

          棘手的部分是,当dragleave 触发时,它的toElementfromElement 似乎与日常生活中的意义相反(这在逻辑上是有意义的,因为它是@ 的倒置动作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】:

                  当文件进入和离开子元素时,它会触发额外的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
                  })
                  

                  【讨论】:

                    【解决方案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() 的值,这对我来说是魔术。

                      【讨论】:

                      • 这应该是一条评论
                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 2012-05-02
                      • 2015-09-03
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多