ファイルを運ぶカーソルがブラウザウィンドウに入るとすぐに、Gmailとまったく同じように、ドロップ領域を強調表示できるようにしたいと思います。しかし、私はそれを機能させることができず、私は本当に明白な何かを見逃しているような気がします。
私はこのようなことをしようとし続けます:
this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
ただし、カーソルがBODY以外の要素の上を移動したり、要素から移動したりするたびにイベントが発生します。これは理にかなっていますが、絶対に機能しません。すべての上に要素を配置してウィンドウ全体を覆い、それを検出することもできますが、それは恐ろしい方法です。
何が足りないのですか?
私はタイムアウトでそれを解決しました(きしむようなクリーンではありませんが、機能します):
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
に設定します。dragleave
でフラグをfalse
に設定します。次に、短いタイムアウトを設定して、フラグがまだfalseであるかどうかを確認します。このように、各dragleave
イベントは、新しいdragover
イベントがフラグをリセットするのに十分な時間をDOMに与えます。 real、finaldragleave
は、フラグがまだfalseであることがわかります。
これがすべての場合に機能するかどうかはわかりませんが、私の場合は非常にうまく機能しました
$('body').bind("dragleave", function(e) {
if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
//outside body / window
}
});
イベントを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);
@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
});
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
は本体ではなかったので、これは気に入らず、戻ります。 this
がbody
の場合、セットは空になり、コードが実行されます。
単純なif (this == $('body').get(0))
で試すことができますが、それはおそらく惨めに失敗します。
私はこれに問題を抱えていて、使用可能な解決策を思いつきましたが、オーバーレイを使用する必要があることに夢中ではありません。
ondragover
、ondragleave
、およびondrop
をウィンドウに追加します
オーバーレイとターゲット要素にondragenter
、ondragleave
、ondrop
を追加します
ウィンドウまたはオーバーレイでドロップが発生した場合、それは無視されますが、ターゲットは必要に応じてドロップを処理します。オーバーレイが必要な理由は、要素がホバーされるたびに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>
ファイルが子要素に出入りすると、追加の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
})
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実装が異なるため(ドラッグアンドドロップ))、これを作成しました。
これが一部の人々に役立つことを本当に願っています。
別の解決策があります。 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
_でファイル数も取得できます
Gmailでドロップゾーンが消えるまでに遅延があることに気づきましたか?私の推測では、ドラッグオーバーまたはそのようなイベントによってリセットされるタイマー(〜500ms)でそれらは消えます。
あなたが説明した問題の核心は、子要素にドラッグした場合でもドラッグリーブがトリガーされることです。これを検出する方法を見つけようとしていますが、エレガントでクリーンなソリューションはまだありません。