Ng-Show is not working properly - html

$scope.stay = function() {
alert("Inside Keep me In")
$scope.timed = false;
$scope.isLogStatus = true;
}
$scope.displayAlert = function() {
$scope.timed = true;
alert("inside display")
}
function idleTimer() {
var t;
$window.onmousemove = resetTimer; // catches mouse movements
$window.onmousedown = resetTimer; // catches mouse movements
$window.onclick = resetTimer; // catches mouse clicks
$window.onscroll = resetTimer;
//window.reload=$scope.stay(); // catches scrolling
// window.onkeypress = resetTimer; //catches keyboard actions
function logout() {
//Adapt to actual logout script
$scope.displayAlert();
alert('insinde logout');
// delete $window.localStorage.user;
// location.href="/";
}
function reload() {
$window.location = self.location.href; //Reloads the current page
}
function resetTimer() {
alert("timer reset")
clearTimeout(t);
// t = setTimeout(logout, 600000); // time is in milliseconds (1000 is 1 second)
$timeout(function() {
alert("timout triggered");
$scope.displayAlert();
}, 9000); // time is in milliseconds (1000 is 1 second)
}
}
idleTimer();
I am using above html and in default if I keep
$scope.timed=true;
it's working
and when I click logged in I am doing
$scope.timed=false;
and again if time is more than 10 minutes I am doing
$scope.timed=true;
(which is not triggering ng-show)
then show is not working
this is controller what is happening
$scope.stay = function() {
alert("Inside Keep me In")
$scope.timed = false;
$scope.isLogStatus = true;
}
$scope.displayAlert = function() {
$scope.timed = true;
alert("inside display")
}
function idleTimer() {
var t;
window.onmousemove = resetTimer; // catches mouse movements
window.onmousedown = resetTimer; // catches mouse movements
window.onclick = resetTimer; // catches mouse clicks
window.onscroll = resetTimer;
//window.reload=$scope.stay(); // catches scrolling
// window.onkeypress = resetTimer; //catches keyboard actions
function logout() {
//Adapt to actual logout script
$scope.displayAlert();
alert('insinde logout');
// delete $window.localStorage.user;
// location.href="/";
}
function reload() {
window.location = self.location.href; //Reloads the current page
}
function resetTimer() {
// alert("timer reset")
clearTimeout(t);
// t = setTimeout(logout, 600000); // time is in milliseconds (1000 is 1 second)
t = setTimeout(logout, 9000); // time is in milliseconds (1000 is 1 second)
}
}
idleTimer();
// Get the topbar menu
$scope.menu = Menus.getMenu('topbar');

The problem is with the way you are calling logout function. You are calling it via outside the angular framework which means that angular will not run the digest cycle and hence any change in scope will not be reflected to the view.
To verify, you can wrap the code in logout function with $timeout. Please do not forget to add it in the dependencies.
$timeout(function(){
$scope.displayAlert();
});
Ideally, you should use angular wrappers e.g. $window for window and $timeout for setTimeout. By doing so angular, automatically, watches for the change and run digest cycle accordingly.

Related

IndexedDB transaction always throwing onabort from add() method in Chrome

I just started experimenting with IndexedDB. I copied an example and pared it down to a small HTML page: Push a button; add a record; dump all the records to the console after the transaction completes.
It runs fine in IE11, but not on Chrome.
The request=transaction.objectstore("store").add({k:v})is always executing the request.onsuccess() method, but the transaction is always resolved with transaction.onabort() by Chrome. Same with .put().
This is the code:
<!DOCTYPE html>
<html>
<head>
<script>
//--- globals
var db;
// The initialization of our stuff in body.onload()
function init() {
var dbVersion = 1;
//--- Try to delete any existing database
var delRequest = indexedDB.deleteDatabase("notesDB");
delRequest.onsuccess = function (de) {
dbOpen(); // .... then open a new one
};
delRequest.onerror = function (de) {
dbOpen(); // ... or open a new one if one doesn't exist to delete
};
function dbOpen () {
var openRequest = indexedDB.open("notesDB", dbVersion);
openRequest.onupgradeneeded = function (e) {
var ldb = e.target.result;
console.log("running onupgradeneeded; always start with a fresh object store");
if (ldb.objectStoreNames.contains("note")) {
ldb.deleteObjectStore("note");
}
if (!ldb.objectStoreNames.contains("note")) {
console.log("creating new note data store");
var objectStore = ldb.createObjectStore("note", { autoIncrement: true });
objectStore.createIndex("title", "title", { unique: false });
}
};
openRequest.onsuccess = function (e) {
db = e.target.result;
db.onerror = function (event) {
// Generic error handler for all errors targeted at this database
alert("Database error: " + event.target.errorCode);
console.dir(event.target);
};
console.log("Database opened; dump existing rows (shouldn't be any)");
displayNotes();
};
openRequest.onerror = function (e) {
console.log("Open error");
console.log(e);
console.dir(e);
};
}
function displayNotes() {
console.log("TODO - print something nice on the page");
var tx = db.transaction("note", "readonly");
tx.oncomplete = function (event) { console.log("read only cursor transaction complete"); }
tx.onerror = function (event) { console.log("readonly transaction onerror"); }
tx.onabort = function (event) { console.log("readonly transaction onabort"); }
// --- iterate cursor
console.log("---Start cursor dump---")
var ds = tx.objectStore("note");
ds.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
console.log(cursor.key);
console.dir(cursor.value);
cursor.continue();
}
else {
console.log("---End cursor dump---");
}
};
}
document.querySelector("#test").addEventListener("click", function (clickevent) {
try {
var transaction = db.transaction("note", "readwrite");
transaction.oncomplete = function (event) {
console.log("Cursor dump in 'add' read/write transaction oncomplete");
displayNotes();
console.log("add transaction oncomplete done!");
};
transaction.onerror = function (event) {
console.log("add transaction onerror");
};
transaction.onabort = function (event) {
console.log("add transaction onabort");
};
var objectStore = transaction.objectStore("note");
var request = objectStore.add({
title: "note header",
body: "this is random note body content " + Math.floor(Math.random() * 1000)
});
request.onsuccess = function (event) {
console.log("add request onsuccess");
};
request.onerror = function (event) {
console.log("add request onerror");
console.dir(event);
};
}
catch (e) {
console.log('catchall exception');
console.log(e);
alert("bad things done");
}
});
}
</script>
</head>
<body onload="init()">
<h1>IndexedDB simplest example</h1>
<p>
<button id="test">Push To Add Row To IndexedDB</button>
</p>
</body>
</html>
I clicked the button a bunch of times and it worked every time.
What error are you getting when it aborts? Look in event.target.error in the onabort handler to see. It could be a QuotaExceededError, which would mean that either you have very low hard drive space or you have a lot of data stored in Chrome for your domain. If that's the case, it's good you're running into it now, because you do need to gracefully handle this case, otherwise users will hit it and be confused.

Is there a generic ScreenSpaceEvent that captures all events?

To react to specific space handlers I typically do this -
var fooHandler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
fooHandler.setInputAction(function(movement){
// do stuff
}, Cesium.ScreenSpaceEventType.WHEEL);
This function would be limited to WHEEL inputs. I have a couple of things that I need to do every time the camera changes position or height. I tried creating an event handler for the camera in a fashion similar to the above, and then calling camera.positionCartographic within that function, but to no avail.
Is there an event in Cesium that captures any movement?
You don't want to use ScreenSpaceEventHandler to do this. Instead, you subscribe to the preRender event and compare the camera position from last frame. Here's some sample code for you:
var lastTime = Cesium.getTimestamp();
var lastPosition = viewer.scene.camera.position.clone();
function preRender(scene) {
var time = Cesium.getTimestamp();
var position = scene.camera.position;
if (!Cesium.Cartesian3.equalsEpsilon(lastPosition, position, Cesium.Math.EPSILON4)) {
document.getElementById('viewChanged').style.display = 'block';
lastTime = time;
} else if (time - lastTime > 250) {
//hide the 'view changed' message after 250 ms of inactivity
lastTime = time;
document.getElementById('viewChanged').style.display = 'none';
}
lastPosition = position.clone();
}
viewer.scene.preRender.addEventListener(preRender);
We plan on adding a viewChanged event to Cesium some time soon, perhaps with 1.8, but this code will continue to work after that and you'll be able to switch to the event at your leisure.
If you want a live demo of the above code, see this port of the view changed Google Earth demo we did in Cesium: http://analyticalgraphicsinc.github.io/cesium-google-earth-examples/examples/viewchangeEvent.html
Here's what I ended up doing:
_preRender = function (scene) {
var currentPosition = scene.camera.position;
if (!Cesium.Cartesian3.equalsEpsilon(_lastPosition, currentPosition, Cesium.Math.EPSILON4)) {
_lastPosition = currentPosition.clone();
if (typeof _positionChangeTimeout !== 'undefined' && _positionChangeTimeout !== null)
{
clearTimeout(_positionChangeTimeout);
}
var currentPositionCartographic = scene.camera.positionCartographic;
_positionChangeTimeout = setTimeout(function() {
if (typeof _positionChangeListener === 'function' && _positionChangeListener !== null)
{
_positionChangeListener({
lat: Cesium.Math.toDegrees(currentPositionCartographic.latitude),
long: Cesium.Math.toDegrees(currentPositionCartographic.longitude),
zoomLevel: _calcZoomForAltitude(currentPositionCartographic.height, currentPositionCartographic.latitude)
});
}
}, 250);
}
}

Vimeo froogaloop api not working when another iframe on page

very strange. the events don't fire when there is another simple iframe. I removed the iframe - and it worked.
the html with the extra iframe:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1255">
<title>Insert title here</title>
</head>
<body>
<iframe id="player_2" src="http://player.vimeo.com/video/XXXXXXX?title=0&byline=0&portrait=0&api=1&player_id=player_2&autoplay=1" width="782" height="413" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>
<iframe src="/homestuff/googleads/index.html" width="1" height="1" style="border:0;" ></iframe>
<script src="/homestuff/froogaloop.min.js"></script>
<script src="/homestuff/vimeoapi-0905.js"></script>
</body>
</html>
vimeo-0905.js is simple event catcher I copied from playground. works perfectly without iframe:
(function(){
// Listen for the ready event for any vimeo video players on the page
var vimeoPlayers = document.querySelectorAll('iframe'),
player;
for (var i = 0, length = vimeoPlayers.length; i < length; i++) {
player = vimeoPlayers[i];
$f(player).addEvent('ready', ready);
}
/**
* Utility function for adding an event. Handles the inconsistencies
* between the W3C method for adding events (addEventListener) and
* IE's (attachEvent).
*/
function addEvent(element, eventName, callback) {
if (element.addEventListener) {
element.addEventListener(eventName, callback, false);
}
else {
element.attachEvent(eventName, callback, false);
}
}
/**
* Called once a vimeo player is loaded and ready to receive
* commands. You can add events and make api calls only after this
* function has been called.
*/
function ready(player_id) {
console.debug('dsadsads');
// Keep a reference to Froogaloop for this player
var container = document.getElementById(player_id).parentNode.parentNode,
froogaloop = $f(player_id);
//apiConsole = container.querySelector('.console .output');
/**
* Sets up the actions for the buttons that will perform simple
* api calls to Froogaloop (play, pause, etc.). These api methods
* are actions performed on the player that take no parameters and
* return no values.
*/
function setupSimpleButtons() {
var buttons = container.querySelector('div .simple'),
playBtn = buttons.querySelector('.play'),
pauseBtn = buttons.querySelector('.pause'),
unloadBtn = buttons.querySelector('.unload');
// Call play when play button clicked
// addEvent(playBtn, 'click', function() {
// froogaloop.api('play');
// }, false);
// Call pause when pause button clicked
addEvent(pauseBtn, 'click', function() {
froogaloop.api('pause');
}, false);
// Call unload when unload button clicked
// addEvent(unloadBtn, 'click', function() {
// froogaloop.api('unload');
// }, false);
}
/**
* Adds listeners for the events that are checked. Adding an event
* through Froogaloop requires the event name and the callback method
* that is called once the event fires.
*/
function setupEventListeners() {
function onPause() {
froogaloop.addEvent('pause', function(data) {
// if (data === 'player_2') $.fancybox.close();
// alert(data);
});
}
function onFinish() {
if (1) {
froogaloop.addEvent('finish', function(data) {
// if (data === 'player_2') $.fancybox.close();
// console.debug('finished');
});
}
else {
froogaloop.removeEvent('finish');
}
}
function onPlayProgress() {
if (1) {
froogaloop.addEvent('playProgress', function(data) {
console.debug(data);
});
}
else {
froogaloop.removeEvent('playProgress');
}
}
window.lastEventHouse = 0.10;
window.lastEventII = 0.1;
// Calls the change event if the option is checked
// (this makes sure the checked events get attached on page load as well as on changed)
onPause();
onFinish();
onPlayProgress();
}
/**
* Sets up actions for adding a new clip window to the page.
*/
function setupAddClip() {
var button = container.querySelector('.addClip'),
newContainer;
addEvent(button, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Gets the index of the current player by simply grabbing the number after the underscore
var currentIndex = parseInt(player_id.split('_')[1]),
clipId = button.querySelector('input').value;
newContainer = resetContainer(container.cloneNode(true), currentIndex+1, clipId);
container.parentNode.appendChild(newContainer);
$f(newContainer.querySelector('iframe')).addEvent('ready', ready);
});
/**
* Resets the duplicate container's information, clearing out anything
* that doesn't pertain to the new clip. It also sets the iframe to
* use the new clip's id as its url.
*/
function resetContainer(element, index, clipId) {
var newHeading = element.querySelector('h2'),
newIframe = element.querySelector('iframe'),
newCheckBoxes = element.querySelectorAll('.listeners input[type="checkbox"]'),
newApiConsole = element.querySelector('.console .output'),
newAddBtn = element.querySelector('.addClip');
// Set the heading text
newHeading.innerText = 'Vimeo Player ' + index;
// Set the correct source of the new clip id
newIframe.src = 'http://player.vimeo.com/video/' + clipId + '?api=1&player_id=player_' + index;
newIframe.id = 'player_' + index;
// Reset all the checkboxes for listeners to be checked on
for (var i = 0, length = newCheckBoxes.length, checkbox; i < length; i++) {
checkbox = newCheckBoxes[i];
checkbox.setAttribute('checked', 'checked');
}
// Clear out the API console
newApiConsole.innerHTML = '';
// Update the clip ID of the add clip button
newAddBtn.querySelector('input').setAttribute('value', clipId);
return element;
}
}
setupEventListeners();
}
})();
I think that the part document.querySelectorAll('iframe') is your problem - you are pretending to the api that all iframes are vimeo. Try picking the player_2 and it should be fine.

Issue with geolocation timeout callback firing whether response received or not

I've implemented a timer for a HTML5 geolocation request as per this post Geolocation feedback while accepting the request
However I'm having an issue where the timer is called regardless of whether the navigator.geolocation.received response is true or not. Perhaps I'm missing something very obvious but I can't see what.
Basically, if I get the geo-location information I run the geo_success_action() function, but in every other case (geo-location failure, timeout on accepting location share, or non-existence of html5 geo-location support in the browser), I want to run the geo_failure_action() function.
However what's happening is that if geo-location is collected, my geo_success_action() function is called, and then when the timer runs out the geo_failure_action() is also called.
I had assumed that within var succeed, the setting of navigator.geolocation.received = true would be passed to my timedout function and therefore if navigator.geolocation.received was true, it wouldn't fire the resulting function.
Any thoughts?
var succeed = function(obj) {
navigator.geolocation.received = true;
if (!navigator.geolocation.timedout) {
geo_success_action( obj, json_url );
}
};
var failed = function(obj) {
navigator.geolocation.received = true;
if (!navigator.geolocation.timedout) {
geo_failure_action( json_url );
} else {
geo_failure_action( json_url );
}
};
var timedout = function() {
navigator.geolocation.timedout = true; // could be used for other callbacks to trace if its timed out or not
if (!navigator.geolocation.received){
geo_failure_action( json_url );
//alert('Timed Out');
} else {
null;
}
}
// Extend geolocation object
if ( navigator.geolocation ) {
navigator.geolocation.retrievePermission = function retrievePermission(succeed,failed,options,timeout) {
this.received = false; // reference for timeout callback
this.timedout = false; // reference for other callbacks
this.getCurrentPosition.apply(this,arguments); // actual request
// Trigger timeout with its function; default timeout offset 5000ms
if ( timeout ) {
setTimeout(timeout.callback,timeout.offset || 5000);
}
}
// New location request with timeout callback
navigator.geolocation.retrievePermission(succeed,failed,{},{
offset: 6000, // miliseconds
callback: timedout
});
// If geo-location not supported at all, do failure action
} else {
geo_failure_action( json_url );
}
I haven't tested, but this should work
var timedout = function() {
navigator.geolocation.timedout = true;
if( navigator.geolocation.received === undefined ) {
navigator.geolocation.received = true;
}
if (!navigator.geolocation.received){
geo_failure_action( json_url );
//alert('Timed Out');
} else {
null;
}
}

How do I detect a HTML5 drag event entering and leaving the window, like Gmail does?

I'd like to be able to highlight the drop area as soon as the cursor carrying a file enters the browser window, exactly the way Gmail does it. But I can't make it work, and I feel like I'm just missing something really obvious.
I keep trying to do something like this:
this.body = $('body').get(0)
this.body.addEventListener("dragenter", this.dragenter, true)
this.body.addEventListener("dragleave", this.dragleave, true)`
But that fires the events whenever the cursor moves over and out of elements other than BODY, which makes sense, but absolutely doesn't work. I could place an element on top of everything, covering the entire window and detect on that, but that'd be a horrible way to go about it.
What am I missing?
I solved it with a timeout (not squeaky-clean, but works):
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 );
});
My example uses jQuery, but it's not necessary. Here's a summary of what's going on:
Set a flag (showDrag) to true on dragenter and dragover of the html (or body) element.
On dragleave set the flag to false. Then set a brief timeout to check if the flag is still false.
Ideally, keep track of the timeout and clear it before setting the next one.
This way, each dragleave event gives the DOM enough time for a new dragover event to reset the flag. The real, final dragleave that we care about will see that the flag is still false.
Modified version from Rehmat (thx)
I liked this idea and instead of writing a new answer, I am updating it here itself. It can be made more precise by checking window dimensions.
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
}
}
Old Version
Don't know it this works for all cases but in my case it worked very well
$('body').bind("dragleave", function(e) {
if (!e.originalEvent.clientX && !e.originalEvent.clientY) {
//outside body / window
}
});
Adding the events to document seemed to work? Tested with Chrome, Firefox, IE 10.
The first element that gets the event is <html>, which should be ok I think.
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);
Here's another solution. I wrote it in React, but I'll explain it at the end if you want to rebuild it in plain JS. It's similar to other answers here, but perhaps slightly more refined.
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;
}
}
So, as others have observed, the dragleave event fires before the next dragenter fires, which means our counter will momentarily hit 0 as we drag files (or whatever) around the page. To prevent that, I've used setImmediate to push the event to the bottom of JavaScript's event queue.
setImmediate isn't well supported, so I wrote my own version which I like better anyway. I haven't seen anyone else implement it quite like this. I use Promise.resolve().then to move the callback to the next tick. This is faster than setImmediate(..., 0) and simpler than many of the other hacks I've seen.
Then the other "trick" I do is to clear/cancel the leave event callback when you drop a file just in case we had a callback pending -- this will prevent the counter from going into the negatives and messing everything up.
That's it. Seems to work very well in my initial testing. No delays, no flashing of my drop target.
Can get the file count too with ev.dataTransfer.items.length
#tyler's answer is the best! I have upvoted it. After spending so many hours I got that suggestion working exactly as intended.
$(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
});
Your third argument to addEventListener is true, which makes the listener run during capture phase (see http://www.w3.org/TR/DOM-Level-3-Events/#event-flow for a visualization). This means that it will capture the events intended for its descendants - and for the body that means all elements on the page. In your handlers, you'll have to check if the element they're triggered for is the body itself. I'll give you my very dirty way of doing it. If anyone knows a simpler way that actually compares elements, I'd love to see it.
this.dragenter = function() {
if ($('body').not(this).length != 0) return;
... functional code ...
}
This finds the body and removes this from the set of elements found. If the set isn't empty, this wasn't the body, so we don't like this and return. If this is body, the set will be empty and the code executes.
You can try with a simple if (this == $('body').get(0)), but that will probably fail miserably.
I was having trouble with this myself and came up with a usable solution, though I'm not crazy about having to use an overlay.
Add ondragover, ondragleave and ondrop to window
Add ondragenter, ondragleave and ondrop to an overlay and a target element
If drop occurs on the window or overlay, it is ignored, whereas the target handles the drop as desired. The reason we need an overlay is because ondragleave triggers every time an element is hovered, so the overlay prevents that from happening, while the drop zone is given a higher z-index so that the files can be dropped. I am using some code snippets found in other drag and drop related questions, so I cannot take full credit. Here's the full 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>
I see a lot of overengineered solutions out there. You should be able to achieve this by simply listening to dragenter and dragleave as your gut seemingly told you.
The tricky part is that when dragleave fires, it seems to have its toElement and fromElement inverted from what makes sense in everyday life (which kind of makes sense in logical terms since it's the inverted action of dragenter).
Bottom-line when you move the cursor from the listening element to outside that element, toElement will have the listening element and fromElement will have the outer non-listening element. In our case, fromElement will be null when we drag outside the browser.
Solution
window.addEventListener("dragleave", function(e){
if (!e.fromElement){
console.log("Dragging back to OS")
}
})
window.addEventListener("dragenter", function(e){
console.log("Dragging to browser")
})
The ondragenter is fired quite often. You can avoid using a helper variable like draggedFile. If you don't care how often your on ondragenter function is being called, you can remove that helper variable.
Solution:
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");
}
}
Have you noticed that there is a delay before the dropzone disappears in Gmail? My guess is that they have it disappear on a timer (~500ms) that gets reset by dragover or some such event.
The core of the problem you described is that dragleave is triggered even when you drag into a child element. I'm trying to find a way to detect this, but I don't have an elegantly clean solution yet.
really sorry to post something that is angular & underscore specific, however the way i solved the problem (HTML5 spec, works on chrome) should be easy to observe.
.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);
});
}
};
})
I use a list to represent the elements that have triggered a drag enter event. when a drag leave event happens i find the element in the drag enter list that matches, remove it from the list, and if the resulting list is empty i know that i have dragged outside of the document/window.
I need to reset the list containing dragged over elements after a drop event occurs, or the next time I start dragging something the list will be populated with elements from the last drag and drop action.
I have only tested this on chrome so far. I made this because Firefox and chrome have different API implementations of HTML5 DND. (drag and drop).
really hope this helps some people.
When the file enters and leaves child elements it fires additional dragenter and dragleave so you need to count up and down.
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
})
I found out from looking at the spec that if the evt.dataTransfer.dropEffect on dragEnd match none then it's a cancelation.
I did already use that event to handle copying without affecting the clipboard. so this was good for me.
When I hit Esc then the drop effect was equal to 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
}
on "dropend" event you can check the value of the document.focus() was the magic trick in my case.