From a491f22c1f2297dcbe7dbd9679593b896584cc87 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 6 Mar 2014 15:16:49 -0600 Subject: [PATCH] fix(backbutton): Allow only one back button listener to run per click, closes #693 --- .../angular/src/service/ionicActionSheet.js | 12 ++-- js/ext/angular/src/service/ionicModal.js | 22 ++---- js/ext/angular/src/service/ionicPlatform.js | 52 +++++++++++++- js/ext/angular/src/service/ionicView.js | 2 +- .../test/service/ionicActionSheet.unit.js | 19 ++--- .../angular/test/service/ionicModal.unit.js | 19 ++--- .../test/service/ionicPlatform.unit.js | 70 +++++++++++++++++-- 7 files changed, 145 insertions(+), 51 deletions(-) diff --git a/js/ext/angular/src/service/ionicActionSheet.js b/js/ext/angular/src/service/ionicActionSheet.js index 227fec9642..77fa5b9692 100644 --- a/js/ext/angular/src/service/ionicActionSheet.js +++ b/js/ext/angular/src/service/ionicActionSheet.js @@ -36,18 +36,14 @@ angular.module('ionic.service.actionSheet', ['ionic.service.templateLoad', 'ioni }); $document[0].body.classList.remove('action-sheet-open'); - }; - var onHardwareBackButton = function() { - hideSheet(); + scope.$deregisterBackButton && scope.$deregisterBackButton(); }; - scope.$on('$destroy', function() { - $ionicPlatform.offHardwareBackButton(onHardwareBackButton); - }); - // Support Android back button to close - $ionicPlatform.onHardwareBackButton(onHardwareBackButton); + scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function(){ + hideSheet(); + }, 300); scope.cancel = function() { hideSheet(true); diff --git a/js/ext/angular/src/service/ionicModal.js b/js/ext/angular/src/service/ionicModal.js index f6e1c597a5..ee69257c25 100644 --- a/js/ext/angular/src/service/ionicModal.js +++ b/js/ext/angular/src/service/ionicModal.js @@ -26,25 +26,13 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv $timeout(function(){ element.addClass('ng-enter-active'); - - if(!self.didInitEvents) { - var onHardwareBackButton = function() { - self.hide(); - }; - - self.scope.$on('$destroy', function() { - $ionicPlatform.offHardwareBackButton(onHardwareBackButton); - }); - - // Support Android back button to close - $ionicPlatform.onHardwareBackButton(onHardwareBackButton); - - self.didInitEvents = true; - } - self.scope.$parent.$broadcast('modal.shown'); }, 20); + self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(function(){ + self.hide(); + }, 200); + }, // Hide the modal hide: function() { @@ -65,6 +53,8 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv ionic.views.Modal.prototype.hide.call(this); this.scope.$parent.$broadcast('modal.hidden'); + + this._deregisterBackButton && this._deregisterBackButton(); }, // Remove and destroy the modal scope diff --git a/js/ext/angular/src/service/ionicPlatform.js b/js/ext/angular/src/service/ionicPlatform.js index ab97ab8c95..be9db1ae73 100644 --- a/js/ext/angular/src/service/ionicPlatform.js +++ b/js/ext/angular/src/service/ionicPlatform.js @@ -12,7 +12,7 @@ angular.module('ionic.service.platform', []) .provider('$ionicPlatform', function() { return { - $get: ['$q', function($q) { + $get: ['$q', '$rootScope', function($q, $rootScope) { return { /** * Some platforms have hardware back buttons, so this is one way to bind to it. @@ -36,6 +36,54 @@ angular.module('ionic.service.platform', []) }); }, + /** + * Register a hardware back button action. Only one action will execute when + * the back button is clicked, so this method decides which of the registered + * back button actions has the highest priority. For example, if an actionsheet + * is showing, the back button should close the actionsheet, but it should not + * also go back a page view or close a modal which may be open. + * + * @param {function} fn the listener function that was originally bound. + * @param {number} priority Only the highest priority will execute. + */ + registerBackButtonAction: function(fn, priority, actionId) { + var self = this; + + if(!self._hasBackButtonHandler) { + // add a back button listener if one hasn't been setup yet + $rootScope.$backButtonActions = {}; + self.onHardwareBackButton(self.hardwareBackButtonClick); + self._hasBackButtonHandler = true; + } + + var action = { + id: (actionId ? actionId : ionic.Utils.nextUid()), + priority: (priority ? priority : 0), + fn: fn + }; + $rootScope.$backButtonActions[action.id] = action; + + // return a function to de-register this back button action + return function() { + delete $rootScope.$backButtonActions[action.id]; + }; + }, + + hardwareBackButtonClick: function(e){ + // loop through all the registered back button actions + // and only run the last one of the highest priority + var priorityAction, actionId; + for(actionId in $rootScope.$backButtonActions) { + if(!priorityAction || $rootScope.$backButtonActions[actionId].priority >= priorityAction.priority) { + priorityAction = $rootScope.$backButtonActions[actionId]; + } + } + if(priorityAction) { + priorityAction.fn(e); + return priorityAction; + } + }, + is: function(type) { return ionic.Platform.is(type); }, @@ -57,7 +105,7 @@ angular.module('ionic.service.platform', []) }; }] }; - + }); })(ionic); diff --git a/js/ext/angular/src/service/ionicView.js b/js/ext/angular/src/service/ionicView.js index 5583e25a48..8ace0d225e 100644 --- a/js/ext/angular/src/service/ionicView.js +++ b/js/ext/angular/src/service/ionicView.js @@ -63,7 +63,7 @@ angular.module('ionic.service.view', ['ui.router', 'ionic.service.platform']) e.preventDefault(); return false; } - $ionicPlatform.onHardwareBackButton(onHardwareBackButton); + $ionicPlatform.registerBackButtonAction(onHardwareBackButton, 100); }]) diff --git a/js/ext/angular/test/service/ionicActionSheet.unit.js b/js/ext/angular/test/service/ionicActionSheet.unit.js index a2c8319c32..2e17f1ef10 100644 --- a/js/ext/angular/test/service/ionicActionSheet.unit.js +++ b/js/ext/angular/test/service/ionicActionSheet.unit.js @@ -1,11 +1,13 @@ describe('Ionic ActionSheet Service', function() { - var sheet, timeout; + var sheet, timeout, ionicPlatform; beforeEach(module('ionic.service.actionSheet')); + beforeEach(module('ionic.service.platform')); - beforeEach(inject(function($ionicActionSheet, $timeout) { + beforeEach(inject(function($ionicActionSheet, $timeout, $ionicPlatform) { sheet = $ionicActionSheet; timeout = $timeout; + ionicPlatform = $ionicPlatform; })); it('Should show', function() { @@ -23,15 +25,10 @@ describe('Ionic ActionSheet Service', function() { expect(wrapper.hasClass('action-sheet-up')).toEqual(true); }); - it('Should handle hardware back button', function() { - // Fake cordova - window.device = {}; - ionic.Platform.isReady = true; + it('should handle hardware back button', function() { var s = sheet.show(); - ionic.trigger('backbutton', { - target: document - }); + ionicPlatform.hardwareBackButtonClick(); expect(s.el.classList.contains('active')).toBe(false); }); @@ -41,9 +38,7 @@ describe('Ionic ActionSheet Service', function() { expect(angular.element(document.body).hasClass('action-sheet-open')).toBe(true); - ionic.trigger('backbutton', { - target: document - }); + ionicPlatform.hardwareBackButtonClick(); expect(angular.element(document.body).hasClass('action-sheet-open')).toBe(false); })); diff --git a/js/ext/angular/test/service/ionicModal.unit.js b/js/ext/angular/test/service/ionicModal.unit.js index c164719f3b..a9f7e7f45b 100644 --- a/js/ext/angular/test/service/ionicModal.unit.js +++ b/js/ext/angular/test/service/ionicModal.unit.js @@ -1,12 +1,15 @@ describe('Ionic Modal', function() { - var modal, q, timeout; + var modal, q, timeout, ionicPlatform, rootScope; beforeEach(module('ionic.service.modal')); + beforeEach(module('ionic.service.platform')); - beforeEach(inject(function($ionicModal, $q, $templateCache, $timeout) { + beforeEach(inject(function($ionicModal, $q, $templateCache, $timeout, $ionicPlatform, $rootScope) { q = $q; modal = $ionicModal; timeout = $timeout; + ionicPlatform = $ionicPlatform; + rootScope = $rootScope; $templateCache.put('modal.html', ''); })); @@ -87,14 +90,12 @@ describe('Ionic Modal', function() { timeout.flush(); - expect(modalInstance.el.classList.contains('active')).toBe(true); + expect(modalInstance.isShown()).toBe(true); - ionic.trigger('backbutton', { - target: document - }); + expect( Object.keys(rootScope.$backButtonActions).length ).toEqual(1); - timeout.flush(); - expect(modalInstance.el.classList.contains('active')).toBe(false); + ionicPlatform.hardwareBackButtonClick(); + expect(modalInstance.isShown()).toBe(false); }); it('should broadcast "modal.shown" on show', function() { @@ -105,6 +106,7 @@ describe('Ionic Modal', function() { timeout.flush(); expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.shown'); }); + it('should broadcast "modal.hidden" on hide', function() { var template = ''; var m = modal.fromTemplate(template, {}); @@ -112,6 +114,7 @@ describe('Ionic Modal', function() { m.hide(); expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.hidden'); }); + it('should broadcast "modal.removed" on remove', inject(function($animate) { var template = ''; var m = modal.fromTemplate(template, {}); diff --git a/js/ext/angular/test/service/ionicPlatform.unit.js b/js/ext/angular/test/service/ionicPlatform.unit.js index bcc427a3af..0888b81fb3 100644 --- a/js/ext/angular/test/service/ionicPlatform.unit.js +++ b/js/ext/angular/test/service/ionicPlatform.unit.js @@ -1,9 +1,13 @@ describe('Ionic Platform Service', function() { - var window; + var window, ionicPlatform, rootScope; - beforeEach(inject(function($window) { + beforeEach(module('ionic.service.platform')); + + beforeEach(inject(function($window, $ionicPlatform, $rootScope) { window = $window; ionic.Platform.ua = ''; + ionicPlatform = $ionicPlatform; + rootScope = $rootScope; })); it('should set platform name', function() { @@ -114,7 +118,7 @@ describe('Ionic Platform Service', function() { window.cordova = {}; ionic.Platform.setPlatform('iOS'); ionic.Platform.setVersion('7.0.3'); - + ionic.Platform._checkPlatforms() expect(ionic.Platform.platforms[0]).toEqual('cordova'); @@ -127,7 +131,7 @@ describe('Ionic Platform Service', function() { window.cordova = {}; ionic.Platform.setPlatform('android'); ionic.Platform.setVersion('4.2.3'); - + ionic.Platform._checkPlatforms() expect(ionic.Platform.platforms[0]).toEqual('cordova'); @@ -243,4 +247,62 @@ describe('Ionic Platform Service', function() { expect(ionic.Platform.is('android')).toEqual(false); }); + it('should register/deregister a hardware back button action and add it to $ionicPlatform.backButtonActions', function() { + var deregisterFn = ionicPlatform.registerBackButtonAction(function(){}); + expect( Object.keys( rootScope.$backButtonActions ).length ).toEqual(1); + deregisterFn(); + expect( Object.keys( rootScope.$backButtonActions ).length ).toEqual(0); + }); + + it('should register multiple back button actions and only call the highest priority on hardwareBackButtonClick', function() { + ionicPlatform.registerBackButtonAction(function(){}, 1, 'action1'); + ionicPlatform.registerBackButtonAction(function(){}, 2, 'action2'); + ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3'); + + var rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp.priority).toEqual(3); + expect(rsp.id).toEqual('action3'); + }); + + it('should register multiple back button actions w/ the same priority and only call the last highest priority on hardwareBackButtonClick', function() { + ionicPlatform.registerBackButtonAction(function(){}, 3, 'action1'); + ionicPlatform.registerBackButtonAction(function(){}, 3, 'action2'); + ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3'); + + var rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp.priority).toEqual(3); + expect(rsp.id).toEqual('action3'); + }); + + it('should register no back button actions and do nothing on hardwareBackButtonClick', function() { + var rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp).toBeUndefined(); + }); + + it('should register multiple back button actions, call hardwareBackButtonClick, deregister, and call hardwareBackButtonClick again', function() { + var dereg1 = ionicPlatform.registerBackButtonAction(function(){}, 1, 'action1'); + var dereg2 = ionicPlatform.registerBackButtonAction(function(){}, 2, 'action2'); + var dereg3 = ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3'); + + var rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp.priority).toEqual(3); + expect(rsp.id).toEqual('action3'); + + dereg3(); + + rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp.priority).toEqual(2); + expect(rsp.id).toEqual('action2'); + + dereg2(); + + rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp.priority).toEqual(1); + expect(rsp.id).toEqual('action1'); + + dereg1(); + rsp = ionicPlatform.hardwareBackButtonClick(); + expect(rsp).toBeUndefined(); + }); + });