From 892160f2001d9094361df9fd8c54aea4a4286bb6 Mon Sep 17 00:00:00 2001 From: Mike Grabski Date: Wed, 20 Nov 2013 01:50:05 -0500 Subject: [PATCH 1/2] create the basics --- .gitattributes | 22 +++++++++++++ .gitignore | 2 ++ bower.json | 26 +++++++++++++++ gruntfile.js | 22 +++++++++++++ karma.conf.js | 70 ++++++++++++++++++++++++++++++++++++++++ package.json | 30 +++++++++++++++++ src/angular-idle.js | 17 ++++++++++ src/angular-idle.spec.js | 9 ++++++ 8 files changed, 198 insertions(+) create mode 100644 .gitattributes create mode 100644 bower.json create mode 100644 gruntfile.js create mode 100644 karma.conf.js create mode 100644 package.json create mode 100644 src/angular-idle.js create mode 100644 src/angular-idle.spec.js diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index a8b26e1..de82815 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,6 @@ results npm-debug.log node_modules +bower_components + .DS_Store diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..73e2876 --- /dev/null +++ b/bower.json @@ -0,0 +1,26 @@ +{ + "name": "ng-idle", + "version": "0.1.0", + "homepage": "https://github.com/HackedByChinese/ng-idle", + "description": "Directives and services for handling idle users in AngularJS", + "main": "src/angular-idle.js", + "keywords": [ + "angularjs", + "idle", + "ng-idle" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "angular": "~1.2.1" + }, + "devDependencies": { + "angular-mocks": "~1.2.1", + } +} diff --git a/gruntfile.js b/gruntfile.js new file mode 100644 index 0000000..b56eb65 --- /dev/null +++ b/gruntfile.js @@ -0,0 +1,22 @@ +module.exports = function(grunt) { + // load all grunt tasks + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); + + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + karma: { + options: { + configFile: 'karma.conf.js' + }, + unit: { + singleRun: true + }, + server: { + autoWatch: true + } + } + }); + + grunt.registerTask('test', ['karma:unit']); + grunt.registerTask('test-server', ['karma:server']); +}; \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..f6eaccf --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,70 @@ +// Karma configuration +// Generated on Tue Nov 19 2013 23:15:01 GMT-0500 (Eastern Standard Time) + +module.exports = function(config) { + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: '', + + + // frameworks to use + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'bower_components/angular/angular.js', + 'bower_components/angular-mocks/angular-mocks.js', + 'src/*.js' + ], + + + // list of files to exclude + exclude: [ + + ], + + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera (has to be installed with `npm install karma-opera-launcher`) + // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) + // - PhantomJS + // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) + browsers: ['Chrome'], + + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4eef12c --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "ng-idle", + "version": "0.1.0", + "description": "Directives and services for responding to idle users in AngularJS", + "scripts": { + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "https://github.com/HackedByChinese/ng-idle.git" + }, + "author": "Mike Grabski ", + "license": "MIT", + "bugs": { + "url": "https://github.com/HackedByChinese/ng-idle/issues" + }, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-jshint": "~0.6.3", + "grunt-contrib-nodeunit": "~0.2.0", + "grunt-contrib-uglify": "~0.2.2", + "grunt-karma": "~0.7.0", + "matchdep": "~0.1.2", + "karma": "~0.10", + "karma-jasmine": "*", + "karma-chrome-launcher": "*", + "karma-firefox-launcher": "*", + "karma-ng-html2js-preprocessor": "*" + } +} diff --git a/src/angular-idle.js b/src/angular-idle.js new file mode 100644 index 0000000..5ec0ac8 --- /dev/null +++ b/src/angular-idle.js @@ -0,0 +1,17 @@ +/** + * Respond to idle users in AngularJS + * @version v0.1.0 + * @link http://hackedbychinese.github.io/ng-idle + * @license MIT License, http://www.opensource.org/licenses/MIT + */ +(function (window, angular, undefined) { + 'use strict'; + + // register modules + var ngIdleSvc = angular.module('ngIdle.services', []); + angular.module('ngIdle', ['ngIdle.services']); + + function $IdleProvider() { + + } +})(window, window.angular); \ No newline at end of file diff --git a/src/angular-idle.spec.js b/src/angular-idle.spec.js new file mode 100644 index 0000000..d6b7f53 --- /dev/null +++ b/src/angular-idle.spec.js @@ -0,0 +1,9 @@ +'use strict'; + +describe('ngIdle.', function() { + // helpers + + describe('services', function() { + beforeEach(module('ngIdle.services')); + }); +}); \ No newline at end of file From 0bb8c3d38ac42bd5f0bec1d573d619710dc508d4 Mon Sep 17 00:00:00 2001 From: Mike Grabski Date: Wed, 27 Nov 2013 20:12:16 -0500 Subject: [PATCH 2/2] service and implementation --- README.md | 71 ++++++++++++++++- bower.json | 3 +- karma.conf.js | 3 +- src/angular-idle.js | 107 ++++++++++++++++++++++++- src/angular-idle.spec.js | 163 ++++++++++++++++++++++++++++++++++++++- test/index.html | 57 ++++++++++++++ 6 files changed, 398 insertions(+), 6 deletions(-) create mode 100644 test/index.html diff --git a/README.md b/README.md index ff70443..f8bfcf8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,73 @@ ng-idle ======= -Services and directives for responding to idle users +## About +Your user may be sitting at the bottom of the ocean like an addled schoolboy (his/her orders are 7 bloody hours old!). You may wish to detect these guys and respond, for example, to log them out so their sensitive data is protected, or taunt them, or whatever. I don't care. + +This module will include a variety of services and directives to help you in this task. + +_**Warning:** This is still in active development and subject to change without noticed. Consider that carefully before including in your production projects. I expect the beta phase to last 1 to 20 years, and that should start in a decade or so._ + +======== + +Authored by Mike Grabski +Licensed under [MIT](http://www.opensource.org/licenses/mit-license.php) + +## Getting Started + +First, you'll need AngularJS 1.2.1 or later (earlier possible, but not tested yet). You can then inject the `$idle` service into your app `run` or in a controller and call `$idle.watch()` when you want to start watching for idleness. You can stop watching anytime by calling `$idle.unwatch()`. `$idle` communicates through events broadcasted on `$rootScope`. + + // include the `ngIdle` module + var app = angular.module('demo', ['ngIdle']); + + app + .controller('EventsCtrl', function($scope, $idle) { + $scope.events = []; + + $scope.$on('$idleStart', function() { + // the user appears to have gone idle + }); + + $scope.$on('$idleWarn', function(e, countdown) { + // follows after the $idleStart event, but includes a countdown until the user is considered timed out + // the countdown arg is the number of seconds remaining until then. + // you can change the title or display a warning dialog from here. + // you can let them resume their session by calling $idle.watch() + }); + + $scope.$on('$idleTimeout', function() { + // the user has timed out (meaning idleDuration + warningDuration has passed without any activity) + // this is where you'd log them + }) + + $scope.$on('$idleEnd', function() { + // the user has come back from AFK and is doing stuff. if you are warning them, you can use this to hide the dialog + }); + + + }) + .config(function($idleProvider) { + // configure $idle settings + $idleProvider.idleDuration(5); + $idleProvider.warningDuration(5); + }) + .run(function($idle){ + // start watching when the app runs + $idle.watch(); + }); + +You can stop watching for idleness at any time by calling `$idle.unwatch()`. + +## Roadmap + +* **0.1**: The basic `$idle` service and `$idleProvider`. + +TBD + +## Contributing + +TBD + +## Developing + +TBD \ No newline at end of file diff --git a/bower.json b/bower.json index 73e2876..d83412c 100644 --- a/bower.json +++ b/bower.json @@ -21,6 +21,7 @@ "angular": "~1.2.1" }, "devDependencies": { - "angular-mocks": "~1.2.1", + "angular-mocks": "~1.2.1", + "jquery": "~2.0.3" } } diff --git a/karma.conf.js b/karma.conf.js index f6eaccf..b0ed714 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -14,8 +14,9 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ + 'bower_components/jquery/jquery.js', 'bower_components/angular/angular.js', - 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-mocks/angular-mocks.js', 'src/*.js' ], diff --git a/src/angular-idle.js b/src/angular-idle.js index 5ec0ac8..adf24a1 100644 --- a/src/angular-idle.js +++ b/src/angular-idle.js @@ -11,7 +11,112 @@ var ngIdleSvc = angular.module('ngIdle.services', []); angular.module('ngIdle', ['ngIdle.services']); + // $idle service and provider function $IdleProvider() { - + + var options = { + idleDuration: 20 * 60, // in seconds (default is 20min) + warningDuration: 30, // in seconds (default is 30sec) + autoResume: true, // lets events automatically resume (unsets idle state/resets warning) + events: 'mousemove keydown DOMMouseScroll mousewheel mousedown' + }; + + this.activeOn = activeOn; + function activeOn (events) { + options.events = events; + }; + + this.idleDuration = idleDuration; + function idleDuration(seconds) { + if (seconds < 0) throw new Error("idleDuration must be a value in seconds, greatner than 0."); + + options.idleDuration = seconds; + } + + this.warningDuration = warningDuration; + function warningDuration(seconds) { + if (seconds < 0) throw new Error("warning must be a value in seconds, greatner than 0."); + + options.warningDuration = seconds; + } + + this.autoResume = autoResume; + function autoResume(value) { + options.autoResume = value === true; + } + + this.$get = $get; + $get.$inject = ['$timeout', '$log', '$rootScope', '$document']; + + function $get($timeout, $log, $rootScope, $document) { + var state = {idle: null, warning: null, idling: false, running: false, countdown: null}; + + function toggleState() { + state.idling = !state.idling; + var name = state.idling ? 'Start' : 'End'; + + $rootScope.$broadcast('$idle' + name); + + if (state.idling) { + state.countdown = options.warningDuration; + countdown(); + } + } + + function countdown() { + if (state.countdown <= 0) { + $rootScope.$broadcast('$idleTimeout'); + } else { + $rootScope.$broadcast('$idleWarn', state.countdown); + + state.warning = $timeout(countdown, 1000); + } + + state.countdown--; + } + + var svc = { + _options: function() { + return options; + }, + _t: function() { + return state.t; + }, + running: function() { + return state.running; + }, + idling: function() { + return state.idling; + }, + watch: function() { + $timeout.cancel(state.idle); + $timeout.cancel(state.warning); + + state.running = true; + + if (state.idling) toggleState(); + + state.idle = $timeout(toggleState, options.idleDuration * 1000); + }, + unwatch: function() { + $timeout.cancel(state.idle); + $timeout.cancel(state.warning); + + state.idling = false; + state.running = false; + } + }; + + var interrupt = function () { + if (state.running && options.autoResume) svc.watch(); + }; + + $document.find('body').on(options.events, interrupt); + + return svc; + }; } + + ngIdleSvc.provider('$idle', $IdleProvider); + })(window, window.angular); \ No newline at end of file diff --git a/src/angular-idle.spec.js b/src/angular-idle.spec.js index d6b7f53..3ff7cdd 100644 --- a/src/angular-idle.spec.js +++ b/src/angular-idle.spec.js @@ -1,9 +1,168 @@ 'use strict'; -describe('ngIdle.', function() { +describe('ngIdle', function() { // helpers - describe('services', function() { + describe('services:', function() { beforeEach(module('ngIdle.services')); + + var $idleProvider, $timeout, $rootScope, $log, $document;; + + beforeEach(function() { + angular.module('app', function() { + }).config(['$idleProvider', function(_$idleProvider_) { + $idleProvider = _$idleProvider_; + }]); + + module('app'); + + inject(function (_$timeout_, _$log_, _$rootScope_, _$document_) { + $rootScope = _$rootScope_; + $timeout = _$timeout_; + $log = _$log_; + $document = _$document_; + }); + }); + + var create = function() { + return $idleProvider.$get($timeout, $log, $rootScope, $document); + }; + + describe('$idleProvider', function() { + + it('activeOn() should update defaults', function() { + expect($idleProvider).not.toBeUndefined(); + + $idleProvider.activeOn('click'); + + expect(create()._options().events).toBe('click'); + }); + + it('idleDuration() should update defaults', function() { + expect($idleProvider).not.toBeUndefined(); + + $idleProvider.idleDuration(500); + + expect(create()._options().idleDuration).toBe(500); + }); + + it('warningDuration() should update defaults', function() { + expect($idleProvider).not.toBeUndefined(); + + $idleProvider.warningDuration(500); + + expect(create()._options().warningDuration).toBe(500); + }); + + it('autoResume() should update defaults', function() { + expect($idleProvider).not.toBeUndefined(); + + $idleProvider.autoResume(false); + + expect(create()._options().autoResume).toBe(false); + }); + }); + + describe('$idle', function() { + var $idle; + + beforeEach(function() { + $idleProvider.warningDuration(3); + $idle = create(); + }); + + afterEach(function() { + + }) + + it ('watch() should clear timeouts and start running', function() { + spyOn($timeout, 'cancel'); + + $idle.watch(); + + expect($timeout.cancel).toHaveBeenCalled(); + expect($idle.running()).toBe(true); + }); + + it ('unwatch() should clear timeouts and stop running', function() { + $idle.watch(); + + spyOn($timeout, 'cancel'); + + $idle.unwatch(); + + expect($timeout.cancel).toHaveBeenCalled(); + expect($idle.running()).toBe(false); + }); + + it ('should broadcast $idleStart', function() { + spyOn($rootScope, '$broadcast'); + + $idle.watch(); + + $timeout.flush(); + + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleStart'); + }); + + it ('should broadcast $idleEnd', function() { + spyOn($rootScope, '$broadcast'); + + $idle.watch(); + + $timeout.flush(); + + $idle.watch(); + + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleEnd'); + }); + + it ('should count down warning and then signal timeout', function() { + spyOn($rootScope, '$broadcast'); + + $idle.watch(); + + $timeout.flush(); + + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleStart'); + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleWarn', 3); + $timeout.flush(); + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleWarn', 2); + $timeout.flush(); + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleWarn', 1); + + $timeout.flush(); + expect($rootScope.$broadcast).toHaveBeenCalledWith('$idleTimeout'); + }); + + it ('watch() should interrupt countdown', function() { + spyOn($rootScope, '$broadcast'); + + $idle.watch(); + $timeout.flush(); + + $timeout.flush(); + + expect($idle.idling()).toBe(true); + + $idle.watch(); + expect($idle.idling()).toBe(false); + }) + +// HACK: the body event listener is only respected the first time, and thus always checks the first $idle instance we created rather than the one we created last. +// in practice, the functionality works fine, but here the test always fails. dunno how to fix it right now. + // it ('document event should interrupt idle timeout', function() { + + // $idle.watch(); + // $timeout.flush(); + + // expect($idle.idling()).toBe(true); + + // var e = $.Event('click'); + // $('body').trigger(e); + + // expect($idle.idling()).toBe(false); + // }); + }); }); }); \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..a3de2fe --- /dev/null +++ b/test/index.html @@ -0,0 +1,57 @@ + + + + + + + +
+

$idle events

+
    +
  • {{event}}
  • +
+
+ + \ No newline at end of file