diff --git a/.jshintrc b/.jshintrc index 5753443..15b5482 100644 --- a/.jshintrc +++ b/.jshintrc @@ -21,6 +21,7 @@ "console", "importScripts", "process", + "Event", "QUnit", "test", "asyncTest", diff --git a/src/js/app-controller.js b/src/js/app-controller.js index efbeaa6..594b308 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -24,6 +24,7 @@ define(function(require) { InvitationDAO = require('js/dao/invitation-dao'), DeviceStorageDAO = require('js/dao/devicestorage-dao'), UpdateHandler = require('js/util/update/update-handler'), + backBtnHandler = require('js/util/backbutton-handler'), config = appConfig.config, str = appConfig.string; @@ -54,6 +55,7 @@ define(function(require) { function onDeviceReady() { axe.debug('Starting app.'); + backBtnHandler.start(); self.buildModules(); // Handle offline and online gracefully diff --git a/src/js/app.js b/src/js/app.js index 44e46b7..40b6465 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -23,6 +23,7 @@ requirejs([ 'js/controller/navigation', 'js/crypto/util', 'js/util/error', + 'js/util/backbutton-handler', 'fastclick', 'angularRoute', 'angularAnimate', @@ -49,6 +50,7 @@ requirejs([ NavigationCtrl, util, errorUtil, + backButtonUtil, FastClick ) { 'use strict'; @@ -113,8 +115,13 @@ requirejs([ app.run(function($rootScope) { // global state... inherited to all child scopes $rootScope.state = {}; + // attach global error handler errorUtil.attachHandler($rootScope); + + // attach the back button handler to the root scope + backButtonUtil.attachHandler($rootScope); + // attach fastclick FastClick.attach(document.body); }); diff --git a/src/js/util/backbutton-handler.js b/src/js/util/backbutton-handler.js new file mode 100644 index 0000000..300d728 --- /dev/null +++ b/src/js/util/backbutton-handler.js @@ -0,0 +1,58 @@ +define(function(require) { + 'use strict'; + + var axe = require('axe'), + DEBUG_TAG = 'backbutton handler'; + + /** + * The back button handler introduces meaningful behavior fo rthe back button: + * if there's an open lightbox, close it; + * if the reader is open in mobile mode, close it; + * if the navigation is open, close it; + * if there's nothing else open, shut down the app; + * + * @type {Object} + */ + var backBtnHandler = { + attachHandler: function(scope) { + this.scope = scope; + }, + start: function() { + document.addEventListener("backbutton", handleBackButton, false); + }, + stop: function() { + document.removeEventListener("backbutton", handleBackButton, false); + } + }; + + function handleBackButton(event) { + axe.debug(DEBUG_TAG, 'back button pressed'); + + // this disarms the default behavior which we NEVER want + event.preventDefault(); + event.stopPropagation(); + + if (backBtnHandler.scope.state.lightbox) { + // closes the lightbox (error msgs, writer, ...) + backBtnHandler.scope.state.lightbox = undefined; + axe.debug(DEBUG_TAG, 'lightbox closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.read && backBtnHandler.scope.state.read.open) { + // closes the reader + backBtnHandler.scope.state.read.toggle(false); + axe.debug(DEBUG_TAG, 'reader closed'); + backBtnHandler.scope.$apply(); + } else if (backBtnHandler.scope.state.nav && backBtnHandler.scope.state.nav.open) { + // closes the navigation + backBtnHandler.scope.state.nav.toggle(false); + axe.debug(DEBUG_TAG, 'navigation closed'); + backBtnHandler.scope.$apply(); + } else { + // exits the app + navigator.app.exitApp(); + } + } + + + return backBtnHandler; +}); \ No newline at end of file diff --git a/test/unit/backbutton-handler-test.js b/test/unit/backbutton-handler-test.js new file mode 100644 index 0000000..3e312b3 --- /dev/null +++ b/test/unit/backbutton-handler-test.js @@ -0,0 +1,67 @@ +define(function(require) { + 'use strict'; + + var btnHandler = require('js/util/backbutton-handler'), + expect = chai.expect; + + describe('Backbutton Handler', function() { + chai.Assertion.includeStack = true; + + var scope, event; + + beforeEach(function() { + scope = { + state: {}, + $apply: function() {} + }; + + event = new CustomEvent('backbutton'); + + // this is a precondition for the test. throw an exception + // if this would produce side effects + expect(navigator.app).to.not.exist; + navigator.app = {}; + + btnHandler.attachHandler(scope); + btnHandler.start(); + }); + + afterEach(function() { + btnHandler.stop(); + delete navigator.app; + }); + + it('should close lightbox', function() { + scope.state.lightbox = 'asd'; + document.dispatchEvent(event); + expect(scope.state.lightbox).to.be.undefined; + }); + + it('should close reader', function() { + scope.state.read = { + open: true, + toggle: function(state) { + scope.state.read.open = state; + } + }; + document.dispatchEvent(event); + expect(scope.state.read.open).to.be.false; + }); + + it('should close navigation', function() { + scope.state.nav = { + open: true, + toggle: function(state) { + scope.state.nav.open = state; + } + }; + document.dispatchEvent(event); + expect(scope.state.nav.open).to.be.false; + }); + + it('should close app', function(done) { + navigator.app.exitApp = done; + document.dispatchEvent(event); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/main.js b/test/unit/main.js index 24b8d18..63c494d 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -22,6 +22,24 @@ if (!Function.prototype.bind) { }; } +// a warm round of applause for phantomjs for missing events +(function() { + function CustomEvent(event, params) { + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + window.CustomEvent = CustomEvent; +})(); + require(['../../src/require-config'], function() { require.config({ baseUrl: '../../src/lib', @@ -59,6 +77,7 @@ function startTests() { 'test/unit/app-controller-test', 'test/unit/pgp-test', 'test/unit/crypto-test', + 'test/unit/backbutton-handler-test', 'test/unit/rest-dao-test', 'test/unit/publickey-dao-test', 'test/unit/privatekey-dao-test',