Skip to content

Commit a491f22

Browse files
author
Adam Bradley
committed
fix(backbutton): Allow only one back button listener to run per click, closes #693
1 parent 78206d0 commit a491f22

File tree

7 files changed

+145
-51
lines changed

7 files changed

+145
-51
lines changed

js/ext/angular/src/service/ionicActionSheet.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,14 @@ angular.module('ionic.service.actionSheet', ['ionic.service.templateLoad', 'ioni
3636
});
3737

3838
$document[0].body.classList.remove('action-sheet-open');
39-
};
4039

41-
var onHardwareBackButton = function() {
42-
hideSheet();
40+
scope.$deregisterBackButton && scope.$deregisterBackButton();
4341
};
4442

45-
scope.$on('$destroy', function() {
46-
$ionicPlatform.offHardwareBackButton(onHardwareBackButton);
47-
});
48-
4943
// Support Android back button to close
50-
$ionicPlatform.onHardwareBackButton(onHardwareBackButton);
44+
scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function(){
45+
hideSheet();
46+
}, 300);
5147

5248
scope.cancel = function() {
5349
hideSheet(true);

js/ext/angular/src/service/ionicModal.js

+6-16
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,13 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv
2626

2727
$timeout(function(){
2828
element.addClass('ng-enter-active');
29-
30-
if(!self.didInitEvents) {
31-
var onHardwareBackButton = function() {
32-
self.hide();
33-
};
34-
35-
self.scope.$on('$destroy', function() {
36-
$ionicPlatform.offHardwareBackButton(onHardwareBackButton);
37-
});
38-
39-
// Support Android back button to close
40-
$ionicPlatform.onHardwareBackButton(onHardwareBackButton);
41-
42-
self.didInitEvents = true;
43-
}
44-
4529
self.scope.$parent.$broadcast('modal.shown');
4630
}, 20);
4731

32+
self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(function(){
33+
self.hide();
34+
}, 200);
35+
4836
},
4937
// Hide the modal
5038
hide: function() {
@@ -65,6 +53,8 @@ angular.module('ionic.service.modal', ['ionic.service.templateLoad', 'ionic.serv
6553
ionic.views.Modal.prototype.hide.call(this);
6654

6755
this.scope.$parent.$broadcast('modal.hidden');
56+
57+
this._deregisterBackButton && this._deregisterBackButton();
6858
},
6959

7060
// Remove and destroy the modal scope

js/ext/angular/src/service/ionicPlatform.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ angular.module('ionic.service.platform', [])
1212
.provider('$ionicPlatform', function() {
1313

1414
return {
15-
$get: ['$q', function($q) {
15+
$get: ['$q', '$rootScope', function($q, $rootScope) {
1616
return {
1717
/**
1818
* Some platforms have hardware back buttons, so this is one way to bind to it.
@@ -36,6 +36,54 @@ angular.module('ionic.service.platform', [])
3636
});
3737
},
3838

39+
/**
40+
* Register a hardware back button action. Only one action will execute when
41+
* the back button is clicked, so this method decides which of the registered
42+
* back button actions has the highest priority. For example, if an actionsheet
43+
* is showing, the back button should close the actionsheet, but it should not
44+
* also go back a page view or close a modal which may be open.
45+
*
46+
* @param {function} fn the listener function that was originally bound.
47+
* @param {number} priority Only the highest priority will execute.
48+
*/
49+
registerBackButtonAction: function(fn, priority, actionId) {
50+
var self = this;
51+
52+
if(!self._hasBackButtonHandler) {
53+
// add a back button listener if one hasn't been setup yet
54+
$rootScope.$backButtonActions = {};
55+
self.onHardwareBackButton(self.hardwareBackButtonClick);
56+
self._hasBackButtonHandler = true;
57+
}
58+
59+
var action = {
60+
id: (actionId ? actionId : ionic.Utils.nextUid()),
61+
priority: (priority ? priority : 0),
62+
fn: fn
63+
};
64+
$rootScope.$backButtonActions[action.id] = action;
65+
66+
// return a function to de-register this back button action
67+
return function() {
68+
delete $rootScope.$backButtonActions[action.id];
69+
};
70+
},
71+
72+
hardwareBackButtonClick: function(e){
73+
// loop through all the registered back button actions
74+
// and only run the last one of the highest priority
75+
var priorityAction, actionId;
76+
for(actionId in $rootScope.$backButtonActions) {
77+
if(!priorityAction || $rootScope.$backButtonActions[actionId].priority >= priorityAction.priority) {
78+
priorityAction = $rootScope.$backButtonActions[actionId];
79+
}
80+
}
81+
if(priorityAction) {
82+
priorityAction.fn(e);
83+
return priorityAction;
84+
}
85+
},
86+
3987
is: function(type) {
4088
return ionic.Platform.is(type);
4189
},
@@ -57,7 +105,7 @@ angular.module('ionic.service.platform', [])
57105
};
58106
}]
59107
};
60-
108+
61109
});
62110

63111
})(ionic);

js/ext/angular/src/service/ionicView.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ angular.module('ionic.service.view', ['ui.router', 'ionic.service.platform'])
6363
e.preventDefault();
6464
return false;
6565
}
66-
$ionicPlatform.onHardwareBackButton(onHardwareBackButton);
66+
$ionicPlatform.registerBackButtonAction(onHardwareBackButton, 100);
6767

6868
}])
6969

js/ext/angular/test/service/ionicActionSheet.unit.js

+7-12
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
describe('Ionic ActionSheet Service', function() {
2-
var sheet, timeout;
2+
var sheet, timeout, ionicPlatform;
33

44
beforeEach(module('ionic.service.actionSheet'));
5+
beforeEach(module('ionic.service.platform'));
56

6-
beforeEach(inject(function($ionicActionSheet, $timeout) {
7+
beforeEach(inject(function($ionicActionSheet, $timeout, $ionicPlatform) {
78
sheet = $ionicActionSheet;
89
timeout = $timeout;
10+
ionicPlatform = $ionicPlatform;
911
}));
1012

1113
it('Should show', function() {
@@ -23,15 +25,10 @@ describe('Ionic ActionSheet Service', function() {
2325
expect(wrapper.hasClass('action-sheet-up')).toEqual(true);
2426
});
2527

26-
it('Should handle hardware back button', function() {
27-
// Fake cordova
28-
window.device = {};
29-
ionic.Platform.isReady = true;
28+
it('should handle hardware back button', function() {
3029
var s = sheet.show();
3130

32-
ionic.trigger('backbutton', {
33-
target: document
34-
});
31+
ionicPlatform.hardwareBackButtonClick();
3532

3633
expect(s.el.classList.contains('active')).toBe(false);
3734
});
@@ -41,9 +38,7 @@ describe('Ionic ActionSheet Service', function() {
4138

4239
expect(angular.element(document.body).hasClass('action-sheet-open')).toBe(true);
4340

44-
ionic.trigger('backbutton', {
45-
target: document
46-
});
41+
ionicPlatform.hardwareBackButtonClick();
4742

4843
expect(angular.element(document.body).hasClass('action-sheet-open')).toBe(false);
4944
}));

js/ext/angular/test/service/ionicModal.unit.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
describe('Ionic Modal', function() {
2-
var modal, q, timeout;
2+
var modal, q, timeout, ionicPlatform, rootScope;
33

44
beforeEach(module('ionic.service.modal'));
5+
beforeEach(module('ionic.service.platform'));
56

6-
beforeEach(inject(function($ionicModal, $q, $templateCache, $timeout) {
7+
beforeEach(inject(function($ionicModal, $q, $templateCache, $timeout, $ionicPlatform, $rootScope) {
78
q = $q;
89
modal = $ionicModal;
910
timeout = $timeout;
11+
ionicPlatform = $ionicPlatform;
12+
rootScope = $rootScope;
1013

1114
$templateCache.put('modal.html', '<div class="modal"></div>');
1215
}));
@@ -87,14 +90,12 @@ describe('Ionic Modal', function() {
8790

8891
timeout.flush();
8992

90-
expect(modalInstance.el.classList.contains('active')).toBe(true);
93+
expect(modalInstance.isShown()).toBe(true);
9194

92-
ionic.trigger('backbutton', {
93-
target: document
94-
});
95+
expect( Object.keys(rootScope.$backButtonActions).length ).toEqual(1);
9596

96-
timeout.flush();
97-
expect(modalInstance.el.classList.contains('active')).toBe(false);
97+
ionicPlatform.hardwareBackButtonClick();
98+
expect(modalInstance.isShown()).toBe(false);
9899
});
99100

100101
it('should broadcast "modal.shown" on show', function() {
@@ -105,13 +106,15 @@ describe('Ionic Modal', function() {
105106
timeout.flush();
106107
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.shown');
107108
});
109+
108110
it('should broadcast "modal.hidden" on hide', function() {
109111
var template = '<div class="modal"></div>';
110112
var m = modal.fromTemplate(template, {});
111113
spyOn(m.scope.$parent, '$broadcast');
112114
m.hide();
113115
expect(m.scope.$parent.$broadcast).toHaveBeenCalledWith('modal.hidden');
114116
});
117+
115118
it('should broadcast "modal.removed" on remove', inject(function($animate) {
116119
var template = '<div class="modal"></div>';
117120
var m = modal.fromTemplate(template, {});

js/ext/angular/test/service/ionicPlatform.unit.js

+66-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
describe('Ionic Platform Service', function() {
2-
var window;
2+
var window, ionicPlatform, rootScope;
33

4-
beforeEach(inject(function($window) {
4+
beforeEach(module('ionic.service.platform'));
5+
6+
beforeEach(inject(function($window, $ionicPlatform, $rootScope) {
57
window = $window;
68
ionic.Platform.ua = '';
9+
ionicPlatform = $ionicPlatform;
10+
rootScope = $rootScope;
711
}));
812

913
it('should set platform name', function() {
@@ -114,7 +118,7 @@ describe('Ionic Platform Service', function() {
114118
window.cordova = {};
115119
ionic.Platform.setPlatform('iOS');
116120
ionic.Platform.setVersion('7.0.3');
117-
121+
118122
ionic.Platform._checkPlatforms()
119123

120124
expect(ionic.Platform.platforms[0]).toEqual('cordova');
@@ -127,7 +131,7 @@ describe('Ionic Platform Service', function() {
127131
window.cordova = {};
128132
ionic.Platform.setPlatform('android');
129133
ionic.Platform.setVersion('4.2.3');
130-
134+
131135
ionic.Platform._checkPlatforms()
132136

133137
expect(ionic.Platform.platforms[0]).toEqual('cordova');
@@ -243,4 +247,62 @@ describe('Ionic Platform Service', function() {
243247
expect(ionic.Platform.is('android')).toEqual(false);
244248
});
245249

250+
it('should register/deregister a hardware back button action and add it to $ionicPlatform.backButtonActions', function() {
251+
var deregisterFn = ionicPlatform.registerBackButtonAction(function(){});
252+
expect( Object.keys( rootScope.$backButtonActions ).length ).toEqual(1);
253+
deregisterFn();
254+
expect( Object.keys( rootScope.$backButtonActions ).length ).toEqual(0);
255+
});
256+
257+
it('should register multiple back button actions and only call the highest priority on hardwareBackButtonClick', function() {
258+
ionicPlatform.registerBackButtonAction(function(){}, 1, 'action1');
259+
ionicPlatform.registerBackButtonAction(function(){}, 2, 'action2');
260+
ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3');
261+
262+
var rsp = ionicPlatform.hardwareBackButtonClick();
263+
expect(rsp.priority).toEqual(3);
264+
expect(rsp.id).toEqual('action3');
265+
});
266+
267+
it('should register multiple back button actions w/ the same priority and only call the last highest priority on hardwareBackButtonClick', function() {
268+
ionicPlatform.registerBackButtonAction(function(){}, 3, 'action1');
269+
ionicPlatform.registerBackButtonAction(function(){}, 3, 'action2');
270+
ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3');
271+
272+
var rsp = ionicPlatform.hardwareBackButtonClick();
273+
expect(rsp.priority).toEqual(3);
274+
expect(rsp.id).toEqual('action3');
275+
});
276+
277+
it('should register no back button actions and do nothing on hardwareBackButtonClick', function() {
278+
var rsp = ionicPlatform.hardwareBackButtonClick();
279+
expect(rsp).toBeUndefined();
280+
});
281+
282+
it('should register multiple back button actions, call hardwareBackButtonClick, deregister, and call hardwareBackButtonClick again', function() {
283+
var dereg1 = ionicPlatform.registerBackButtonAction(function(){}, 1, 'action1');
284+
var dereg2 = ionicPlatform.registerBackButtonAction(function(){}, 2, 'action2');
285+
var dereg3 = ionicPlatform.registerBackButtonAction(function(){}, 3, 'action3');
286+
287+
var rsp = ionicPlatform.hardwareBackButtonClick();
288+
expect(rsp.priority).toEqual(3);
289+
expect(rsp.id).toEqual('action3');
290+
291+
dereg3();
292+
293+
rsp = ionicPlatform.hardwareBackButtonClick();
294+
expect(rsp.priority).toEqual(2);
295+
expect(rsp.id).toEqual('action2');
296+
297+
dereg2();
298+
299+
rsp = ionicPlatform.hardwareBackButtonClick();
300+
expect(rsp.priority).toEqual(1);
301+
expect(rsp.id).toEqual('action1');
302+
303+
dereg1();
304+
rsp = ionicPlatform.hardwareBackButtonClick();
305+
expect(rsp).toBeUndefined();
306+
});
307+
246308
});

0 commit comments

Comments
 (0)