web-dev-qa-db-ja.com

Gmailのように、ウィンドウに出入りするHTML5ドラッグイベントを検出するにはどうすればよいですか?

ファイルを運ぶカーソルがブラウザウィンドウに入るとすぐに、Gmailとまったく同じように、ドロップ領域を強調表示できるようにしたいと思います。しかし、私はそれを機能させることができず、私は本当に明白な何かを見逃しているような気がします。

私はこのようなことをしようとし続けます:

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

ただし、カーソルがBODY以外の要素の上を移動したり、要素から移動したりするたびにイベントが発生します。これは理にかなっていますが、絶対に機能しません。すべての上に要素を配置してウィンドウ全体を覆い、それを検出することもできますが、それは恐ろしい方法です。

何が足りないのですか?

23
Heilemann

私はタイムアウトでそれを解決しました(きしむようなクリーンではありませんが、機能します):

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(またはbody)要素のshowDragおよびtrueのフラグ(dragenter)をdragoverに設定します。
  • dragleaveでフラグをfalseに設定します。次に、短いタイムアウトを設定して、フラグがまだfalseであるかどうかを確認します。
  • 理想的には、タイムアウトを追跡し、次のタイムアウトを設定する前にそれをクリアします。

このように、各dragleaveイベントは、新しいdragoverイベントがフラグをリセットするのに十分な時間をDOMに与えます。 real、finaldragleaveは、フラグがまだfalseであることがわかります。

24
Tyler

これがすべての場合に機能するかどうかはわかりませんが、私の場合は非常にうまく機能しました

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

イベントを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);
7
ile

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

addEventListenerに対する3番目の引数は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
MvanGeest

私はこれに問題を抱えていて、使用可能な解決策を思いつきましたが、オーバーレイを使用する必要があることに夢中ではありません。

ondragoverondragleave、およびondropをウィンドウに追加します

オーバーレイとターゲット要素に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>
1
Mathachew

ファイルが子要素に出入りすると、追加の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
})
0
AJcodez

angular&アンダースコア固有のものを投稿して本当に申し訳ありませんが、問題を解決した方法(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ではHTML5DNDのAPI実装が異なるため(ドラッグアンドドロップ))、これを作成しました。

これが一部の人々に役立つことを本当に願っています。

0
PPPaul

別の解決策があります。 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)よりも高速で、私が見た他の多くのハックよりも簡単です。

次に、私が行うもう1つの「トリック」は、コールバックが保留になっている場合に備えて、ファイルをドロップしたときにLeaveイベントコールバックをクリア/キャンセルすることです。これにより、カウンターがネガティブになり、すべてが台無しになるのを防ぎます。

それでおしまい。私の最初のテストでは非常にうまく機能しているようです。遅延も、ドロップターゲットの点滅もありません。


_ev.dataTransfer.items.length_でファイル数も取得できます

0
mpen

Gmailでドロップゾーンが消えるまでに遅延があることに気づきましたか?私の推測では、ドラッグオーバーまたはそのようなイベントによってリセットされるタイマー(〜500ms)でそれらは消えます。

あなたが説明した問題の核心は、子要素にドラッグした場合でもドラッグリーブがトリガーされることです。これを検出する方法を見つけようとしていますが、エレガントでクリーンなソリューションはまだありません。

0
dlo