Skip to content

Commit

Permalink
fix(backbutton): Allow only one back button listener to run per click,
Browse files Browse the repository at this point in the history
…closes #693
  • Loading branch information
Adam Bradley committed Mar 6, 2014
1 parent 78206d0 commit a491f22
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 51 deletions.
12 changes: 4 additions & 8 deletions js/ext/angular/src/service/ionicActionSheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
22 changes: 6 additions & 16 deletions js/ext/angular/src/service/ionicModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand Down
52 changes: 50 additions & 2 deletions js/ext/angular/src/service/ionicPlatform.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
},
Expand All @@ -57,7 +105,7 @@ angular.module('ionic.service.platform', [])
};
}]
};

});

})(ionic);
2 changes: 1 addition & 1 deletion js/ext/angular/src/service/ionicView.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}])

Expand Down
19 changes: 7 additions & 12 deletions js/ext/angular/test/service/ionicActionSheet.unit.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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);
});
Expand All @@ -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);
}));
Expand Down
19 changes: 11 additions & 8 deletions js/ext/angular/test/service/ionicModal.unit.js
Original file line number Diff line number Diff line change
@@ -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>');
}));
Expand Down Expand Up @@ -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() {
Expand All @@ -105,13 +106,15 @@ 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, {});
spyOn(m.scope.$parent, '$broadcast');
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, {});
Expand Down
70 changes: 66 additions & 4 deletions js/ext/angular/test/service/ionicPlatform.unit.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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();
});

});

0 comments on commit a491f22

Please sign in to comment.