Skip to content

Commit e86b331

Browse files
committed
fix(popup): fix race conditions and memory leaks
Closes #2815
1 parent 008df7b commit e86b331

File tree

2 files changed

+67
-60
lines changed

2 files changed

+67
-60
lines changed

Diff for: js/angular/service/popup.js

+65-58
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
119119
stackPushDelay: 75
120120
};
121121
var popupStack = [];
122+
122123
var $ionicPopup = {
123124
/**
124125
* @ngdoc method
@@ -288,8 +289,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
288289
$ionicTemplateLoader.load(options.templateUrl) :
289290
$q.when(options.template || options.content || '');
290291

291-
return $q.all([popupPromise, contentPromise])
292-
.then(function(results) {
292+
return $q.all([popupPromise, contentPromise]).then(function(results) {
293293
var self = results[0];
294294
var content = results[1];
295295
var responseDeferred = $q.defer();
@@ -322,7 +322,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
322322
});
323323

324324
self.show = function() {
325-
if (self.isShown) return;
325+
if (self.isShown || self.removed) return;
326326

327327
self.isShown = true;
328328
ionic.requestAnimationFrame(function() {
@@ -334,6 +334,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
334334
focusInput(self.element);
335335
});
336336
};
337+
337338
self.hide = function(callback) {
338339
callback = callback || noop;
339340
if (!self.isShown) return callback();
@@ -343,6 +344,7 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
343344
self.element.addClass('popup-hidden');
344345
$timeout(callback, 250);
345346
};
347+
346348
self.remove = function() {
347349
if (self.removed) return;
348350

@@ -359,74 +361,79 @@ function($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicB
359361
}
360362

361363
function onHardwareBackButton(e) {
362-
popupStack[0] && popupStack[0].responseDeferred.resolve();
364+
var last = popupStack[popupStack.length - 1];
365+
last && last.responseDeferred.resolve();
363366
}
364367

365368
function showPopup(options) {
369+
var resultDeferred;
366370
var popupPromise = $ionicPopup._createPopup(options);
367-
var previousPopup = popupStack[0];
368371

369-
if (previousPopup) {
370-
previousPopup.hide();
372+
if (popupStack.length > 0) {
373+
popupStack[popupStack.length - 1].hide();
374+
resultDeferred = $timeout(doShowPopup, config.stackPushDelay);
375+
} else {
376+
//Add popup-open & backdrop if this is first popup
377+
$ionicBody.addClass('popup-open');
378+
$ionicBackdrop.retain();
379+
//only show the backdrop on the first popup
380+
$ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
381+
onHardwareBackButton,
382+
PLATFORM_BACK_BUTTON_PRIORITY_POPUP
383+
);
384+
resultDeferred = doShowPopup();
371385
}
372386

373-
var resultPromise = $timeout(noop, previousPopup ? config.stackPushDelay : 0)
374-
.then(function() { return popupPromise; })
375-
.then(function(popup) {
376-
if (!previousPopup) {
377-
//Add popup-open & backdrop if this is first popup
378-
$ionicBody.addClass('popup-open');
379-
$ionicBackdrop.retain();
380-
//only show the backdrop on the first popup
381-
$ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
382-
onHardwareBackButton,
383-
PLATFORM_BACK_BUTTON_PRIORITY_POPUP
384-
);
385-
}
386-
popupStack.unshift(popup);
387-
popup.show();
388-
389-
//DEPRECATED: notify the promise with an object with a close method
390-
popup.responseDeferred.notify({
391-
close: resultPromise.close
387+
resultDeferred.close = function popupClose(result) {
388+
popupPromise.then(function(popup) {
389+
if (!popup.removed) popup.responseDeferred.resolve(result);
392390
});
391+
};
393392

394-
return popup.responseDeferred.promise.then(function(result) {
395-
var index = popupStack.indexOf(popup);
396-
if (index !== -1) {
397-
popupStack.splice(index, 1);
398-
}
399-
popup.remove();
400-
401-
var previousPopup = popupStack[0];
402-
if (previousPopup) {
403-
previousPopup.show();
404-
} else {
405-
//Remove popup-open & backdrop if this is last popup
406-
$timeout(function() {
407-
// wait to remove this due to a 300ms delay native
408-
// click which would trigging whatever was underneath this
409-
$ionicBody.removeClass('popup-open');
410-
}, 400);
411-
$timeout(function() {
412-
$ionicBackdrop.release();
413-
}, config.stackPushDelay || 0);
414-
($ionicPopup._backButtonActionDone || noop)();
415-
}
416-
return result;
417-
});
418-
});
393+
return resultDeferred;
394+
395+
function doShowPopup() {
396+
return popupPromise.then(function(popup) {
397+
popupStack.push(popup);
398+
popup.show();
399+
400+
//DEPRECATED: notify the promise with an object with a close method
401+
popup.responseDeferred.notify({
402+
close: resultDeferred.close
403+
});
404+
405+
return popup.responseDeferred.promise.then(function(result) {
406+
var index = popupStack.indexOf(popup);
407+
if (index !== -1) {
408+
popupStack.splice(index, 1);
409+
}
410+
popup.remove();
411+
412+
if (popupStack.length > 0) {
413+
popupStack[popupStack.length - 1].show();
414+
} else {
415+
//Remove popup-open & backdrop if this is last popup
416+
$timeout(function() {
417+
// wait to remove this due to a 300ms delay native
418+
// click which would trigging whatever was underneath this
419+
if (!popupStack.length) {
420+
$ionicBody.removeClass('popup-open');
421+
}
422+
}, 400, false);
423+
$timeout(function() {
424+
if (!popupStack.length) $ionicBackdrop.release();
425+
}, config.stackPushDelay || 0, false);
426+
427+
($ionicPopup._backButtonActionDone || noop)();
428+
}
429+
430+
return result;
431+
});
419432

420-
function close(result) {
421-
popupPromise.then(function(popup) {
422-
if (!popup.removed) {
423-
popup.responseDeferred.resolve(result);
424-
}
425433
});
434+
426435
}
427-
resultPromise.close = close;
428436

429-
return resultPromise;
430437
}
431438

432439
function focusInput(element) {

Diff for: test/unit/angular/service/popup.unit.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ describe('$ionicPopup service', function() {
277277
expect($ionicBackdrop.release).toHaveBeenCalled();
278278
expect(document.body.classList.contains('popup-open')).toBe(false);
279279
}));
280-
it('template should only overwrite prompt input if it includes html', inject(function($timeout) {
281-
spyOn($ionicPopup, '_createPopup');
280+
it('template should only overwrite prompt input if it includes html', inject(function($timeout, $q) {
281+
spyOn($ionicPopup, '_createPopup').andCallThrough();
282282
$ionicPopup.prompt({template: "Tacos!"});
283283
params = $ionicPopup._createPopup.mostRecentCall.args;
284284
expect(params[0].template.indexOf('<span>Tacos!</span>')).toEqual(0);

0 commit comments

Comments
 (0)