Как обнаружить событие перетаскивания HTML5, входящее и выходящее из окна, как это делает Gmail? - PullRequest
22 голосов
/ 30 июня 2010

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

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

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

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

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

Ответы [ 10 ]

22 голосов
/ 10 января 2013

Я решил это с тайм-аутом (не брезгливым, но работает):

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.Затем установите короткий тайм-аут, чтобы проверить, установлен ли флаг по-прежнему.
  • В идеале, отследите тайм-аут и очистите его, прежде чем устанавливать следующий.Событие 1020 * дает DOM достаточно времени для нового события dragover, чтобы сбросить флаг. реальное, окончательное dragleave, о котором мы заботимся, увидит, что флаг все еще ложен.
8 голосов
/ 01 марта 2016

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

$('body').bind("dragleave", function(e) {
   if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
      //outside body / window
   }
});
6 голосов
/ 17 сентября 2014

Добавление событий в 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 голосов
/ 03 июля 2013

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

$(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 голос
/ 23 мая 2012

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

Добавить 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>
1 голос
/ 30 июня 2010

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

0 голосов
/ 28 марта 2019

Вот еще одно решение.Я написал это в 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

0 голосов
/ 19 декабря 2017

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

очень жаль публиковать что-то угловатое и подчеркивающее, однако то, как я решил проблему (спецификация 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);
    });
  }
};

})

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

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

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

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

0 голосов
/ 21 мая 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...