diff --git a/.travis.yml b/.travis.yml index 910e5dd..9bb31d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false language: node_js node_js: - - "0.10" + - "5.2.0" before_script: - npm install -g grunt-cli diff --git a/README.md b/README.md index dcbb953..6757960 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Ng-Idle [![Join the chat at https://gitter.im/HackedByChinese/ng-idle](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/HackedByChinese/ng-idle?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/HackedByChinese/ng-idle.png?branch=master)](https://travis-ci.org/HackedByChinese/ng-idle) +**Angular 2 Developers**: This module is for Angular 1 only. Check out [ng2-idle](https://github.com/HackedByChinese/ng2-idle) for the Angular 2 version. + ## About You may wish to detect idle users and respond, for example, to log them out so their sensitive data is protected, or taunt them, or whatever. I don't care. diff --git a/angular-idle.js b/angular-idle.js index fbbfc50..42a9c18 100644 --- a/angular-idle.js +++ b/angular-idle.js @@ -1,6 +1,6 @@ /*** Directives and services for responding to idle users in AngularJS * @author Mike Grabski -* @version v1.1.1 +* @version v1.2.0 * @link https://github.com/HackedByChinese/ng-idle.git * @license MIT */ @@ -85,6 +85,7 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) timeout: 30, // in seconds (default is 30sec) autoResume: 'idle', // lets events automatically resume (unsets idle state/resets warning) interrupt: 'mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll', + windowInterrupt: null, keepalive: true }; @@ -102,6 +103,10 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) options.interrupt = events; }; + this.windowInterrupt = function(events) { + options.windowInterrupt = events; + }; + var setIdle = this.idle = function(seconds) { if (seconds <= 0) throw new Error('Idle must be a value in seconds, greater than 0.'); @@ -148,8 +153,6 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) state.idling = !state.idling; var name = state.idling ? 'Start' : 'End'; - $rootScope.$broadcast('Idle' + name); - if (state.idling) { stopKeepalive(); if (options.timeout) { @@ -161,10 +164,19 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) startKeepalive(); } + $rootScope.$broadcast('Idle' + name); + $interval.cancel(state.idle); } function countdown() { + + // check not called when no longer idling + // possible with multiple tabs + if(!state.idling){ + return; + } + // countdown has expired, so signal timeout if (state.countdown <= 0) { timeout(); @@ -262,7 +274,7 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) stopKeepalive(); }, - interrupt: function(noExpiryUpdate) { + interrupt: function(anotherTab) { if (!state.running) return; if (options.timeout && this.isExpired()) { @@ -271,7 +283,24 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) } // note: you can no longer auto resume once we exceed the expiry; you will reset state by calling watch() manually - if (options.autoResume === 'idle' || (options.autoResume === 'notIdle' && !state.idling)) this.watch(noExpiryUpdate); + if (anotherTab || options.autoResume === 'idle' || (options.autoResume === 'notIdle' && !state.idling)) this.watch(anotherTab); + } + }; + + var lastMove = { + clientX: null, + clientY: null, + swap: function(event) { + var last = {clientX: this.clientX, clientY: this.clientY}; + this.clientX = event.clientX; + this.clientY = event.clientY; + return last; + }, + hasMoved: function(event) { + var last = this.swap(event); + if (this.clientX === null || event.movementX || event.movementY) return true; + else if (last.clientX != event.clientX || last.clientY != event.clientY) return true; + else return false; } }; @@ -280,21 +309,23 @@ angular.module('ngIdle.idle', ['ngIdle.keepalive', 'ngIdle.localStorage']) return; // Fix for Chrome desktop notifications, triggering mousemove event. } - /* - note: - webkit fires fake mousemove events when the user has done nothing, so the idle will never time out while the cursor is over the webpage - Original webkit bug report which caused this issue: - https://bugs.webkit.org/show_bug.cgi?id=17052 - Chromium bug reports for issue: - https://code.google.com/p/chromium/issues/detail?id=5598 - https://code.google.com/p/chromium/issues/detail?id=241476 - https://code.google.com/p/chromium/issues/detail?id=317007 - */ - if (event.type !== 'mousemove' || angular.isUndefined(event.movementX) || (event.movementX || event.movementY)) { + if (event.type !== 'mousemove' || lastMove.hasMoved(event)) { svc.interrupt(); } }); + if(options.windowInterrupt) { + var eventList = options.windowInterrupt.split(' '); + var fn = function() { + svc.interrupt(); + }; + + for(var i=0; i -* @version v1.1.1 +* @version v1.2.0 * @link https://github.com/HackedByChinese/ng-idle.git * @license MIT */ -!function(a,b,c){"use strict";b.module("ngIdle",["ngIdle.keepalive","ngIdle.idle","ngIdle.countdown","ngIdle.title","ngIdle.localStorage"]),b.module("ngIdle.keepalive",[]).provider("Keepalive",function(){var a={http:null,interval:600};this.http=function(c){if(!c)throw new Error("Argument must be a string containing a URL, or an object containing the HTTP request configuration.");b.isString(c)&&(c={url:c,method:"GET"}),c.cache=!1,a.http=c};var c=this.interval=function(b){if(b=parseInt(b),isNaN(b)||0>=b)throw new Error("Interval must be expressed in seconds and be greater than 0.");a.interval=b};this.$get=["$rootScope","$log","$interval","$http",function(d,e,f,g){function h(a,b){d.$broadcast("KeepaliveResponse",a,b)}function i(){d.$broadcast("Keepalive"),b.isObject(a.http)&&g(a.http).success(h).error(h)}var j={ping:null};return{_options:function(){return a},setInterval:c,start:function(){return f.cancel(j.ping),j.ping=f(i,1e3*a.interval),j.ping},stop:function(){f.cancel(j.ping)},ping:function(){i()}}}]}),b.module("ngIdle.idle",["ngIdle.keepalive","ngIdle.localStorage"]).provider("Idle",function(){var a={idle:1200,timeout:30,autoResume:"idle",interrupt:"mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll",keepalive:!0},c=this.timeout=function(c){if(c===!1)a.timeout=0;else{if(!(b.isNumber(c)&&c>=0))throw new Error("Timeout must be zero or false to disable the feature, or a positive integer (in seconds) to enable it.");a.timeout=c}};this.interrupt=function(b){a.interrupt=b};var d=this.idle=function(b){if(0>=b)throw new Error("Idle must be a value in seconds, greater than 0.");a.idle=b};this.autoResume=function(b){b===!0?a.autoResume="idle":b===!1?a.autoResume="off":a.autoResume=b},this.keepalive=function(b){a.keepalive=b===!0},this.$get=["$interval","$log","$rootScope","$document","Keepalive","IdleLocalStorage","$window",function(e,f,g,h,i,j,k){function l(){a.keepalive&&(t.running&&i.ping(),i.start())}function m(){a.keepalive&&i.stop()}function n(){t.idling=!t.idling;var b=t.idling?"Start":"End";g.$broadcast("Idle"+b),t.idling?(m(),a.timeout&&(t.countdown=a.timeout,o(),t.timeout=e(o,1e3,a.timeout,!1))):l(),e.cancel(t.idle)}function o(){return t.countdown<=0?void p():(g.$broadcast("IdleWarn",t.countdown),void t.countdown--)}function p(){m(),e.cancel(t.idle),e.cancel(t.timeout),t.idling=!0,t.running=!1,t.countdown=0,g.$broadcast("IdleTimeout")}function q(a,b,c){var d=a.running();a.unwatch(),b(c),d&&a.watch()}function r(){var a=j.get("expiry");return a&&a.time?new Date(a.time):null}function s(a){a?j.set("expiry",{id:u,time:a}):j.remove("expiry")}var t={idle:null,timeout:null,idling:!1,running:!1,countdown:null},u=(new Date).getTime(),v={_options:function(){return a},_getNow:function(){return new Date},getIdle:function(){return a.idle},getTimeout:function(){return a.timeout},setIdle:function(a){q(this,d,a)},setTimeout:function(a){q(this,c,a)},isExpired:function(){var a=r();return null!==a&&a<=this._getNow()},running:function(){return t.running},idling:function(){return t.idling},watch:function(b){e.cancel(t.idle),e.cancel(t.timeout);var c=a.timeout?a.timeout:0;b||s(new Date((new Date).getTime()+1e3*(a.idle+c))),t.idling?n():t.running||l(),t.running=!0,t.idle=e(n,1e3*a.idle,0,!1)},unwatch:function(){e.cancel(t.idle),e.cancel(t.timeout),t.idling=!1,t.running=!1,s(null),m()},interrupt:function(b){return t.running?a.timeout&&this.isExpired()?void p():void(("idle"===a.autoResume||"notIdle"===a.autoResume&&!t.idling)&&this.watch(b)):void 0}};h.find("html").on(a.interrupt,function(a){"mousemove"===a.type&&a.originalEvent&&0===a.originalEvent.movementX&&0===a.originalEvent.movementY||("mousemove"!==a.type||b.isUndefined(a.movementX)||a.movementX||a.movementY)&&v.interrupt()});var w=function(a){if("ngIdle.expiry"===a.key&&a.newValue&&a.newValue!==a.oldValue){var c=b.fromJson(a.newValue);if(c.id===u)return;v.interrupt(!0)}};return k.addEventListener?k.addEventListener("storage",w,!1):k.attachEvent("onstorage",w),v}]}),b.module("ngIdle.countdown",["ngIdle.idle"]).directive("idleCountdown",["Idle",function(a){return{restrict:"A",scope:{value:"=idleCountdown"},link:function(b){b.value=a.getTimeout(),b.$on("IdleWarn",function(a,c){b.$evalAsync(function(){b.value=c})}),b.$on("IdleTimeout",function(){b.$evalAsync(function(){b.value=0})})}}}]),b.module("ngIdle.title",[]).provider("Title",function(){function a(a,b,c){return new Array(b-String(a).length+1).join(c||"0")+a}var c={enabled:!0},d=this.enabled=function(a){c.enabled=a===!0};this.$get=["$document","$interpolate",function(e,f){var g={original:null,idle:"{{minutes}}:{{seconds}} until your session times out!",timedout:"Your session has expired."};return{setEnabled:d,isEnabled:function(){return c.enabled},original:function(a){return b.isUndefined(a)?g.original:void(g.original=a)},store:function(a){(a||!g.original)&&(g.original=this.value())},value:function(a){return b.isUndefined(a)?e[0].title:void(e[0].title=a)},idleMessage:function(a){return b.isUndefined(a)?g.idle:void(g.idle=a)},timedOutMessage:function(a){return b.isUndefined(a)?g.timedout:void(g.timedout=a)},setAsIdle:function(b){this.store();var c={totalSeconds:b};c.minutes=Math.floor(b/60),c.seconds=a(b-60*c.minutes,2),this.value(f(this.idleMessage())(c))},setAsTimedOut:function(){this.store(),this.value(this.timedOutMessage())},restore:function(){this.original()&&this.value(this.original())}}}]}).directive("title",["Title",function(a){return{restrict:"E",link:function(b,c,d){a.isEnabled()&&!d.idleDisabled&&(a.store(!0),b.$on("IdleStart",function(){a.original(c[0].innerText)}),b.$on("IdleWarn",function(b,c){a.setAsIdle(c)}),b.$on("IdleEnd",function(){a.restore()}),b.$on("IdleTimeout",function(){a.setAsTimedOut()}))}}}]),b.module("ngIdle.localStorage",[]).service("IdleStorageAccessor",["$window",function(a){return{get:function(){return a.localStorage}}}]).service("IdleLocalStorage",["IdleStorageAccessor",function(a){function d(){var a={};this.setItem=function(b,c){a[b]=c},this.getItem=function(b){return"undefined"!=typeof a[b]?a[b]:null},this.removeItem=function(b){a[b]=c}}function e(){try{var b=a.get();return b.setItem("ngIdleStorage",""),b.removeItem("ngIdleStorage"),b}catch(c){return new d}}var f=e();return{set:function(a,c){f.setItem("ngIdle."+a,b.toJson(c))},get:function(a){return b.fromJson(f.getItem("ngIdle."+a))},remove:function(a){f.removeItem("ngIdle."+a)},_wrapped:function(){return f}}}])}(window,window.angular); +!function(a,b,c){"use strict";b.module("ngIdle",["ngIdle.keepalive","ngIdle.idle","ngIdle.countdown","ngIdle.title","ngIdle.localStorage"]),b.module("ngIdle.keepalive",[]).provider("Keepalive",function(){var a={http:null,interval:600};this.http=function(c){if(!c)throw new Error("Argument must be a string containing a URL, or an object containing the HTTP request configuration.");b.isString(c)&&(c={url:c,method:"GET"}),c.cache=!1,a.http=c};var c=this.interval=function(b){if(b=parseInt(b),isNaN(b)||0>=b)throw new Error("Interval must be expressed in seconds and be greater than 0.");a.interval=b};this.$get=["$rootScope","$log","$interval","$http",function(d,e,f,g){function h(a,b){d.$broadcast("KeepaliveResponse",a,b)}function i(){d.$broadcast("Keepalive"),b.isObject(a.http)&&g(a.http).success(h).error(h)}var j={ping:null};return{_options:function(){return a},setInterval:c,start:function(){return f.cancel(j.ping),j.ping=f(i,1e3*a.interval),j.ping},stop:function(){f.cancel(j.ping)},ping:function(){i()}}}]}),b.module("ngIdle.idle",["ngIdle.keepalive","ngIdle.localStorage"]).provider("Idle",function(){var a={idle:1200,timeout:30,autoResume:"idle",interrupt:"mousemove keydown DOMMouseScroll mousewheel mousedown touchstart touchmove scroll",windowInterrupt:null,keepalive:!0},c=this.timeout=function(c){if(c===!1)a.timeout=0;else{if(!(b.isNumber(c)&&c>=0))throw new Error("Timeout must be zero or false to disable the feature, or a positive integer (in seconds) to enable it.");a.timeout=c}};this.interrupt=function(b){a.interrupt=b},this.windowInterrupt=function(b){a.windowInterrupt=b};var d=this.idle=function(b){if(0>=b)throw new Error("Idle must be a value in seconds, greater than 0.");a.idle=b};this.autoResume=function(b){b===!0?a.autoResume="idle":b===!1?a.autoResume="off":a.autoResume=b},this.keepalive=function(b){a.keepalive=b===!0},this.$get=["$interval","$log","$rootScope","$document","Keepalive","IdleLocalStorage","$window",function(e,f,g,h,i,j,k){function l(){a.keepalive&&(t.running&&i.ping(),i.start())}function m(){a.keepalive&&i.stop()}function n(){t.idling=!t.idling;var b=t.idling?"Start":"End";t.idling?(m(),a.timeout&&(t.countdown=a.timeout,o(),t.timeout=e(o,1e3,a.timeout,!1))):l(),g.$broadcast("Idle"+b),e.cancel(t.idle)}function o(){if(t.idling){if(t.countdown<=0)return void p();g.$broadcast("IdleWarn",t.countdown),t.countdown--}}function p(){m(),e.cancel(t.idle),e.cancel(t.timeout),t.idling=!0,t.running=!1,t.countdown=0,g.$broadcast("IdleTimeout")}function q(a,b,c){var d=a.running();a.unwatch(),b(c),d&&a.watch()}function r(){var a=j.get("expiry");return a&&a.time?new Date(a.time):null}function s(a){a?j.set("expiry",{id:u,time:a}):j.remove("expiry")}var t={idle:null,timeout:null,idling:!1,running:!1,countdown:null},u=(new Date).getTime(),v={_options:function(){return a},_getNow:function(){return new Date},getIdle:function(){return a.idle},getTimeout:function(){return a.timeout},setIdle:function(a){q(this,d,a)},setTimeout:function(a){q(this,c,a)},isExpired:function(){var a=r();return null!==a&&a<=this._getNow()},running:function(){return t.running},idling:function(){return t.idling},watch:function(b){e.cancel(t.idle),e.cancel(t.timeout);var c=a.timeout?a.timeout:0;b||s(new Date((new Date).getTime()+1e3*(a.idle+c))),t.idling?n():t.running||l(),t.running=!0,t.idle=e(n,1e3*a.idle,0,!1)},unwatch:function(){e.cancel(t.idle),e.cancel(t.timeout),t.idling=!1,t.running=!1,s(null),m()},interrupt:function(b){return t.running?a.timeout&&this.isExpired()?void p():void((b||"idle"===a.autoResume||"notIdle"===a.autoResume&&!t.idling)&&this.watch(b)):void 0}},w={clientX:null,clientY:null,swap:function(a){var b={clientX:this.clientX,clientY:this.clientY};return this.clientX=a.clientX,this.clientY=a.clientY,b},hasMoved:function(a){var b=this.swap(a);return null===this.clientX||a.movementX||a.movementY?!0:b.clientX!=a.clientX||b.clientY!=a.clientY?!0:!1}};if(h.find("html").on(a.interrupt,function(a){"mousemove"===a.type&&a.originalEvent&&0===a.originalEvent.movementX&&0===a.originalEvent.movementY||("mousemove"!==a.type||w.hasMoved(a))&&v.interrupt()}),a.windowInterrupt)for(var x=a.windowInterrupt.split(" "),y=function(){v.interrupt()},z=0;z