Как определить событие перетаскивания HTML5, входящее и выходящее из окна, например Gmail?

Я хочу, чтобы можно было выделить область выделения, как только курсор, переносящий файл, войдет в окно браузера, точно так же, как это делает Gmail. Но я не могу заставить его работать, и мне кажется, что я просто пропущу что-то действительно очевидное.

Я продолжаю пытаться сделать что-то вроде этого:

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

Но это вызывает события, когда курсор перемещается и выходит за пределы элементов, отличных от BODY, что имеет смысл, но абсолютно не работает. Я мог бы разместить элемент поверх всего, покрывая все окно и обнаруживая это, но это был бы ужасный способ сделать это.

Что мне не хватает?

Ответ 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, но это необязательно. Вот краткое описание того, что происходит:

  • Установите флаг (showDrag) на true на dragenter и dragover элемента html (или body).
  • В dragleave установите флаг false. Затем установите короткий тайм-аут, чтобы проверить, остается ли флаг false.
  • В идеале, отслеживайте таймаут и очищайте его перед установкой следующего.

Таким образом, каждое событие dragleave дает DOM достаточное время для нового события dragover для reset флага. Реальный, окончательный dragleave, который нас волнует, увидит, что флаг по-прежнему остается ложным.

Ответ 2

Не знаю, это работает для всех случаев, но в моем случае это работало очень хорошо

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

Ответ 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

Ваш третий аргумент 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)), но это, вероятно, потерпит неудачу.

Ответ 6

У меня возникли проблемы с этим самим и придумал полезное решение, хотя я не сумасшедший, чтобы использовать оверлей.

Добавьте ondragover, ondragleave и ondrop в окно

Добавьте ondragenter, ondragleave и ondrop к наложению и целевому элементу

Если в окне или наложении происходит падение, оно игнорируется, тогда как цель обрабатывает падение по желанию. Причина, по которой нам требуется оверлей, состоит в том, что ondragleave запускается каждый раз, когда элемент зависает, поэтому наложение предотвращает это, тогда как в зоне выпадения указан более высокий индекс z, чтобы файлы можно было отбросить. Я использую некоторые фрагменты кода, найденные в других вопросах, связанных с перетаскиванием, поэтому я не могу получить полный кредит. Здесь полный 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>

Ответ 7

Вы заметили, что существует задержка до того, как dropzone исчезнет в Gmail? Я предполагаю, что они исчезают на таймере (~ 500 мс), который получает reset с помощью dragover или какого-то такого события.

Ядро проблемы, которую вы описали, заключается в том, что dragleave запускается даже при перетаскивании в дочерний элемент. Я пытаюсь найти способ обнаружить это, но пока у меня нет элегантно чистого решения.

Ответ 8

действительно жаль опубликовать что-то, что есть angular, и подчеркнуть специфику, однако, как я решил проблему (спецификация HTML5, работает на хроме), должно быть легко наблюдать.

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

})

Я использую список для представления элементов, вызвавших событие перетаскивания. когда происходит событие перетаскивания, я нахожу элемент в списке перетаскивания, который соответствует, удаляет его из списка, и если результирующий список пуст, я знаю, что я перетащил его за пределы документа/окна.

Мне нужно reset список, содержащий перетаскиваемые элементы после события пересылки, или в следующий раз, когда я начну перетаскивать что-то, список будет заполнен элементами из последнего действия перетаскивания.

Я тестировал это только на хроме. Я сделал это, потому что Firefox и хром имеют разные версии API HTML5 DND. (перетаскивание).

действительно надеюсь, что это поможет некоторым людям.

Ответ 9

Когда файл вводит и оставляет дочерние элементы, он запускает дополнительные 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
})

Ответ 10

Здесь другое решение. Я написал это в 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 чтобы setImmediate событие в setImmediate очереди событий JavaScript.

setImmediate не очень хорошо поддерживается, поэтому я написал свою собственную версию, которая мне все равно нравится больше. Я не видел, чтобы кто-нибудь еще реализовал это так. Promise.resolve().then я использую Promise.resolve().then Чтобы переместить обратный вызов на следующий тик. Это быстрее, чем setImmediate(..., 0) и проще, чем многие другие хаки, которые я видел.

Затем другой "трюк", который я делаю, - это очистить/отменить обратный вызов события покидания, когда вы удаляете файл на случай, если у нас был отложенный обратный вызов - это предотвратит переход счетчика в негативы и все испортит.

Это. Кажется, работает очень хорошо в моем первоначальном тестировании. Никаких задержек, никаких всплесков моей цели.


Может получить количество файлов тоже с ev.dataTransfer.items.length