fix(backbutton): Allow only one back button listener to run per click, closes #693

This commit is contained in:
Adam Bradley
2014-03-06 15:16:49 -06:00
parent 78206d0e7c
commit a491f22c1f
7 changed files with 145 additions and 51 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}])

View File

@@ -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);
}));

View File

@@ -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', '<div class="modal"></div>');
}));
@@ -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 = '<div class="modal"></div>';
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 = '<div class="modal"></div>';
var m = modal.fromTemplate(template, {});

View File

@@ -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();
});
});