From 62c9ea8540c1586afd8428ef2bd38f5eecfb95e8 Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Wed, 16 Sep 2015 09:57:26 -0500 Subject: [PATCH] fix(interimElement): support scope.$destroy events when navigation (eg $location) changes occur, the `scope.$destroy` event is triggered; which should remove/close/cleanup the following components: menu, select, dialog, toast, and bottomSheet. Fixes #3741. Fixes #4405. Fixes #4504. Fixes #4151. Closes #4659. --- src/components/bottomSheet/bottom-sheet.js | 12 +- .../bottomSheet/bottom-sheet.spec.js | 21 +- .../bottomSheet/demoBasicUsage/script.js | 4 +- src/components/dialog/dialog.js | 47 +++- src/components/dialog/dialog.spec.js | 22 ++ src/components/menu/js/menuController.js | 9 +- src/components/menu/js/menuDirective.js | 7 +- src/components/menu/js/menuServiceProvider.js | 42 +-- src/components/menu/menu.scss | 1 - src/components/menu/menu.spec.js | 17 +- src/components/select/select.js | 107 ++++---- src/components/select/select.spec.js | 240 +++++++++--------- src/components/toast/toast.js | 14 +- .../services/interimElement/interimElement.js | 131 +++++++--- 14 files changed, 424 insertions(+), 250 deletions(-) diff --git a/src/components/bottomSheet/bottom-sheet.js b/src/components/bottomSheet/bottom-sheet.js index 382e4eea608..fb8d78fdfd0 100644 --- a/src/components/bottomSheet/bottom-sheet.js +++ b/src/components/bottomSheet/bottom-sheet.js @@ -12,9 +12,17 @@ angular .directive('mdBottomSheet', MdBottomSheetDirective) .provider('$mdBottomSheet', MdBottomSheetProvider); -function MdBottomSheetDirective() { +/* @ngInject */ +function MdBottomSheetDirective($mdBottomSheet) { return { - restrict: 'E' + restrict: 'E', + link : function postLink(scope, element, attr) { + // When navigation force destroys an interimElement, then + // listen and $destroy() that interim instance... + scope.$on('$destroy', function() { + $mdBottomSheet.destroy(); + }); + } }; } diff --git a/src/components/bottomSheet/bottom-sheet.spec.js b/src/components/bottomSheet/bottom-sheet.spec.js index f98f5fa152a..57e43eb8ec4 100644 --- a/src/components/bottomSheet/bottom-sheet.spec.js +++ b/src/components/bottomSheet/bottom-sheet.spec.js @@ -2,7 +2,7 @@ describe('$mdBottomSheet service', function() { beforeEach(module('material.components.bottomSheet')); describe('#build()', function() { - it('should escapeToClose == true', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) { + it('should close when `escapeToClose == true`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) { var parent = angular.element('
'); $mdBottomSheet.show({ template: '', @@ -24,7 +24,7 @@ describe('$mdBottomSheet service', function() { expect(parent.find('md-bottom-sheet').length).toBe(0); })); - it('should escapeToClose == false', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) { + it('should not close when `escapeToClose == false`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) { var parent = angular.element('
'); $mdBottomSheet.show({ template: '', @@ -42,6 +42,23 @@ describe('$mdBottomSheet service', function() { expect(parent.find('md-bottom-sheet').length).toBe(1); })); + it('should close when navigation fires `scope.$destroy()`', inject(function($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) { + var parent = angular.element('
'); + $mdBottomSheet.show({ + template: '', + parent: parent, + escapeToClose: false + }); + + $rootScope.$apply(); + $animate.triggerCallbacks(); + + expect(parent.find('md-bottom-sheet').length).toBe(1); + + $rootScope.$destroy(); + expect(parent.find('md-bottom-sheet').length).toBe(0); + })); + it('should focus child with md-autofocus', inject(function($rootScope, $animate, $document, $mdBottomSheet) { jasmine.mockElementFocus(this); var parent = angular.element('
'); diff --git a/src/components/bottomSheet/demoBasicUsage/script.js b/src/components/bottomSheet/demoBasicUsage/script.js index d49fe28d702..b8fdde4291b 100644 --- a/src/components/bottomSheet/demoBasicUsage/script.js +++ b/src/components/bottomSheet/demoBasicUsage/script.js @@ -22,7 +22,7 @@ angular.module('bottomSheetDemo1', ['ngMaterial']) controller: 'ListBottomSheetCtrl', targetEvent: $event }).then(function(clickedItem) { - $scope.alert = clickedItem.name + ' clicked!'; + $scope.alert = clickedItem['name'] + ' clicked!'; }); }; @@ -35,7 +35,7 @@ angular.module('bottomSheetDemo1', ['ngMaterial']) }).then(function(clickedItem) { $mdToast.show( $mdToast.simple() - .content(clickedItem.name + ' clicked!') + .content(clickedItem['name'] + ' clicked!') .position('top right') .hideDelay(1500) ); diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 3090ed51889..541c3c0f4b7 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -10,7 +10,7 @@ angular .directive('mdDialog', MdDialogDirective) .provider('$mdDialog', MdDialogProvider); -function MdDialogDirective($$rAF, $mdTheming) { +function MdDialogDirective($$rAF, $mdTheming, $mdDialog) { return { restrict: 'E', link: function(scope, element, attr) { @@ -25,9 +25,19 @@ function MdDialogDirective($$rAF, $mdTheming) { //-- delayed image loading may impact scroll height, check after images are loaded angular.element(images).on('load', addOverflowClass); } + + scope.$on('$destroy', function() { + $mdDialog.destroy(); + }); + + /** + * + */ function addOverflowClass() { element.toggleClass('md-content-overflow', content.scrollHeight > content.clientHeight); } + + }); } }; @@ -530,16 +540,30 @@ function MdDialogProvider($$interimElementProvider) { function onRemove(scope, element, options) { options.deactivateListeners(); options.unlockScreenReader(); + options.hideBackdrop(options.$destroy); - options.hideBackdrop(); + // For navigation $destroy events, do a quick, non-animated removal, + // but for normal closes (from clicks, etc) animate the removal - return dialogPopOut(element, options) - .finally(function() { - angular.element($document[0].body).removeClass('md-dialog-is-showing'); - element.remove(); + return !!options.$destroy ? detachAndClean() : animateRemoval().then( detachAndClean ); - options.origin.focus(); - }); + /** + * For normal closes, animate the removal. + * For forced closes (like $destroy events), skip the animations + */ + function animateRemoval() { + return dialogPopOut(element, options); + } + + /** + * Detach the element + */ + function detachAndClean() { + angular.element($document[0].body).removeClass('md-dialog-is-showing'); + element.remove(); + + if (!options.$destroy) options.origin.focus(); + } } /** @@ -661,10 +685,12 @@ function MdDialogProvider($$interimElementProvider) { /** * Hide modal backdrop element... */ - options.hideBackdrop = function hideBackdrop() { + options.hideBackdrop = function hideBackdrop($destroy) { if (options.backdrop) { - $animate.leave(options.backdrop); + if ( !!$destroy ) options.backdrop.remove(); + else $animate.leave(options.backdrop); } + if (options.disableParentScroll) { options.restoreScroll(); delete options.restoreScroll; @@ -755,7 +781,6 @@ function MdDialogProvider($$interimElementProvider) { var isFixed = $window.getComputedStyle($document[0].body).position == 'fixed'; var backdrop = options.backdrop ? $window.getComputedStyle(options.backdrop[0]) : null; - var height = backdrop ? Math.min($document[0].body.clientHeight, Math.ceil(Math.abs(parseInt(backdrop.height, 10)))) : 0; container.css({ diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index ced5a33c829..bd4ab3e8994 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -158,6 +158,28 @@ describe('$mdDialog', function() { expect(container.length).toBe(0); })); + it('should remove `md-dialog-container` on scope.$destroy()', inject(function($mdDialog, $rootScope, $timeout) { + var container, parent = angular.element('
'); + + $mdDialog.show( + $mdDialog.alert({ + template: '' + + '' + + ' ' + + '

Muppets are the best

' + + '
' + + '
', + parent: parent + }) + ); + + runAnimation(parent.find('md-dialog')); + $rootScope.$destroy(); + container = angular.element(parent[0].querySelector('.md-dialog-container')); + + expect(container.length).toBe(0); + })); + }); describe('#confirm()', function() { diff --git a/src/components/menu/js/menuController.js b/src/components/menu/js/menuController.js index 7ccfc6f88cb..9c418604618 100644 --- a/src/components/menu/js/menuController.js +++ b/src/components/menu/js/menuController.js @@ -100,7 +100,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) { preserveElement: self.isInMenuBar || self.nestedMenus.length > 0, parent: self.isInMenuBar ? $element : 'body' }); - } + }; // Expose a open function to the child scope for html to use $scope.$mdOpenMenu = this.open; @@ -133,6 +133,10 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) { this.containerProxy && this.containerProxy(ev); }; + this.destroy = function() { + return $mdMenu.destroy(); + }; + // Use the $mdMenu interim element service to close the menu contents this.close = function closeMenu(skipFocus, closeOpts) { if ( !self.isOpen ) return; @@ -140,12 +144,13 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) { $scope.$emit('$mdMenuClose', $element); $mdMenu.hide(null, closeOpts); + if (!skipFocus) { var el = self.restoreFocusTo || $element.find('button')[0]; if (el instanceof angular.element) el = el[0]; el.focus(); } - } + }; /** * Build a nice object out of our string attribute which specifies the diff --git a/src/components/menu/js/menuDirective.js b/src/components/menu/js/menuDirective.js index 0b027658bbf..da60e0180c9 100644 --- a/src/components/menu/js/menuDirective.js +++ b/src/components/menu/js/menuDirective.js @@ -191,8 +191,11 @@ function MenuDirective($mdUtil) { mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar }); scope.$on('$destroy', function() { - menuContainer.remove(); - mdMenuCtrl.close(); + mdMenuCtrl + .destroy() + .finally(function(){ + menuContainer.remove(); + }); }); } diff --git a/src/components/menu/js/menuServiceProvider.js b/src/components/menu/js/menuServiceProvider.js index 982ff9c5ba1..7832af83c68 100644 --- a/src/components/menu/js/menuServiceProvider.js +++ b/src/components/menu/js/menuServiceProvider.js @@ -22,7 +22,7 @@ function MenuProvider($$interimElementProvider) { }); /* @ngInject */ - function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate, $timeout) { + function menuDefaultOptions($mdUtil, $mdTheming, $mdConstant, $document, $window, $q, $$rAF, $animateCss, $animate) { var animator = $mdUtil.dom.animator; return { @@ -63,14 +63,8 @@ function MenuProvider($$interimElementProvider) { * Hide and destroys the backdrop created by showBackdrop() */ return function hideBackdrop() { - if (options.backdrop) { - // Override duration to immediately remove invisible backdrop - options.backdrop.off('click'); - $animate.leave(options.backdrop, {duration:0}); - } - if (options.disableParentScroll) { - options.restoreScroll(); - } + if (options.backdrop) options.backdrop.remove(); + if (options.disableParentScroll) options.restoreScroll(); }; } @@ -83,14 +77,28 @@ function MenuProvider($$interimElementProvider) { opts.cleanupResizing(); opts.hideBackdrop(); - return $animateCss(element, {addClass: 'md-leave'}) - .start() - .then(function() { - element.removeClass('md-active'); + // For navigation $destroy events, do a quick, non-animated removal, + // but for normal closes (from clicks, etc) animate the removal + + return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean ); + + /** + * For normal closes, animate the removal. + * For forced closes (like $destroy events), skip the animations + */ + function animateRemoval() { + return $animateCss(element, {addClass: 'md-leave'}).start(); + } + + /** + * Detach the element + */ + function detachAndClean() { + element.removeClass('md-active'); + detachElement(element, opts); + opts.alreadyOpen = false; + } - detachElement(element, opts); - opts.alreadyOpen = false; - }); } /** @@ -361,7 +369,7 @@ function MenuProvider($$interimElementProvider) { } /** - * Use browser to remove this element without triggering a $destory event + * Use browser to remove this element without triggering a $destroy event */ function detachElement(element, opts) { if (!opts.preserveElement) { diff --git a/src/components/menu/menu.scss b/src/components/menu/menu.scss index 301b817f63a..940405e1d5d 100644 --- a/src/components/menu/menu.scss +++ b/src/components/menu/menu.scss @@ -17,7 +17,6 @@ $max-dense-menu-height: 2 * $baseline-grid + $max-visible-items * $dense-menu-it margin-top: $baseline-grid / 2; margin-bottom: $baseline-grid / 2; height: 1px; - min-height: 1px; width: 100%; } diff --git a/src/components/menu/menu.spec.js b/src/components/menu/menu.spec.js index a6b8fb8c4aa..6c649a7ce55 100644 --- a/src/components/menu/menu.spec.js +++ b/src/components/menu/menu.spec.js @@ -86,6 +86,7 @@ describe('material.components.menu', function() { }); it('closes on backdrop click', inject(function($document) { + openMenu(setup()); expect(getOpenMenuContainer().length).toBe(1); @@ -96,6 +97,7 @@ describe('material.components.menu', function() { expect(getOpenMenuContainer().length).toBe(0); })); + it('closes on escape', inject(function($document, $mdConstant) { openMenu(setup()); expect(getOpenMenuContainer().length).toBe(1); @@ -108,6 +110,16 @@ describe('material.components.menu', function() { expect(getOpenMenuContainer().length).toBe(0); })); + it('closes on $destroy', inject(function($document, $rootScope) { + var scope = $rootScope.$new(); + openMenu( setup(null,false,scope) ); + + expect(getOpenMenuContainer().length).toBe(1); + scope.$destroy(); + + expect(getOpenMenuContainer().length).toBe(0); + })); + describe('closes with -', function() { it('closes on normal option click', function() { expect(getOpenMenuContainer().length).toBe(0); @@ -164,7 +176,7 @@ describe('material.components.menu', function() { } }); - function setup(triggerType, noEvent) { + function setup(triggerType, noEvent, scope) { var menu, template = $mdUtil.supplant('' + '' + @@ -180,7 +192,7 @@ describe('material.components.menu', function() { $rootScope.doSomething = function($event) { menuActionPerformed = true; }; - menu = $compile(template)($rootScope); + menu = $compile(template)(scope || $rootScope); }); attachedElements.push(menu); @@ -193,7 +205,6 @@ describe('material.components.menu', function() { // Internal methods // ******************************************** - function getOpenMenuContainer() { var res; inject(function($document) { diff --git a/src/components/select/select.js b/src/components/select/select.js index 225ce216aee..517c615250c 100755 --- a/src/components/select/select.js +++ b/src/components/select/select.js @@ -144,7 +144,6 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par attr.tabindex = attr.tabindex || '0'; return function postLink(scope, element, attr, ctrls) { - var isOpen; var isDisabled; var containerCtrl = ctrls[0]; @@ -316,20 +315,23 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par element.attr(ariaAttrs); scope.$on('$destroy', function() { - if (isOpen) { - $mdSelect.hide().finally(function() { - selectContainer.remove(); + $mdSelect + .destroy() + .finally(function() { + if ( selectContainer ) { + selectContainer.remove(); + } + + if (containerCtrl) { + containerCtrl.setFocused(false); + containerCtrl.setHasValue(false); + containerCtrl.input = null; + } }); - } else { - selectContainer.remove(); - } - if (containerCtrl) { - containerCtrl.setFocused(false); - containerCtrl.setHasValue(false); - containerCtrl.input = null; - } }); + + function inputCheckValue() { // The select counts as having a value if one or more options are selected, // or if the input's validity state says it has bad input (eg string in a number input) @@ -345,7 +347,8 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par selectScope = scope.$new(); $mdTheming.inherit(selectContainer, element); if (element.attr('md-container-class')) { - selectContainer[0].setAttribute('class', selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class')); + var value = selectContainer[0].getAttribute('class') + ' ' + element.attr('md-container-class'); + selectContainer[0].setAttribute('class', value); } selectContainer = $compile(selectContainer)(selectScope); selectMenuCtrl = selectContainer.find('md-select-menu').controller('mdSelectMenu'); @@ -374,7 +377,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par } function openSelect() { - scope.$apply('isOpen = true'); + selectScope.isOpen = true; $mdSelect.show({ scope: selectScope, @@ -385,7 +388,7 @@ function SelectDirective($mdSelect, $mdUtil, $mdTheming, $mdAria, $compile, $par hasBackdrop: true, loadingAsync: attr.mdOnOpen ? scope.$eval(attr.mdOnOpen) || true : false }).then(function() { - isOpen = false; + selectScope.isOpen = false; }); } }; @@ -782,38 +785,42 @@ function SelectProvider($$interimElementProvider) { * Interim-element onRemove logic.... */ function onRemove(scope, element, opts) { + opts = opts || { }; opts.cleanupInteraction(); opts.cleanupResizing(); opts.hideBackdrop(); - return $animateCss(element, {addClass: 'md-leave'}) - .start() - .then(function(response) { - - configureAria(opts.target, false); - element.removeClass('md-active'); - - announceClosed(opts); - detachElement(element, opts); - - return response; - }) - .finally(function() { - opts.restoreFocus && opts.target.focus(); - }); - - // If we want to ignore leave animations (and remove immediately): - // - // configureAria(opts.target, false); - // - // element.addClass('md-leave'); - // element.removeClass('md-active'); - // - // announceClosed(opts); - // detachElement(element, opts); - // opts.restoreFocus && opts.target.focus(); - // - // return $q.when(true); + // For navigation $destroy events, do a quick, non-animated removal, + // but for normal closes (from clicks, etc) animate the removal + + return (opts.$destroy === true) ? detachAndClean() : animateRemoval().then( detachAndClean ); + + /** + * For normal closes (eg clicks), animate the removal. + * For forced closes (like $destroy events from navigation), + * skip the animations + */ + function animateRemoval() { + return $animateCss(element, {addClass: 'md-leave'}).start(); + } + + /** + * Detach the element and cleanup prior changes + */ + function detachAndClean() { + configureAria(opts.target, false); + + element.attr('opacity', 0); + element.removeClass('md-active'); + detachElement(element, opts); + + announceClosed(opts); + + if (!opts.$destroy && opts.restoreFocus) { + opts.target.focus(); + } + } + } /** @@ -911,14 +918,10 @@ function SelectProvider($$interimElementProvider) { * Hide modal backdrop element... */ return function hideBackdrop() { - if (options.backdrop) { - // Override duration to immediately remove invisible backdrop - $animate.leave(options.backdrop, {duration: 0}); - } - if (options.disableParentScroll) { - options.restoreScroll(); - delete options.restoreScroll; - } + if (options.backdrop) options.backdrop.remove(); + if (options.disableParentScroll) options.restoreScroll(); + + delete options.restoreScroll; } } @@ -1164,7 +1167,7 @@ function SelectProvider($$interimElementProvider) { } /** - * Use browser to remove this element without triggering a $destory event + * Use browser to remove this element without triggering a $destroy event */ function detachElement(element, opts) { if (element[0].parentNode === opts.parent[0]) { diff --git a/src/components/select/select.spec.js b/src/components/select/select.spec.js index 44f6d5ea890..0e5117f28c9 100755 --- a/src/components/select/select.spec.js +++ b/src/components/select/select.spec.js @@ -1,116 +1,18 @@ describe('', function() { var attachedElements = []; - afterEach(function() { - attachedElements.forEach(function(element) { - element.remove(); - }); - attachedElements = []; - }); - beforeEach(module('material.components.input')); beforeEach(module('material.components.select')); - - function setupSelect(attrs, options, bNoLabel) { - var el; - - inject(function($compile, $rootScope) { - var src = ''; - if (!bNoLabel) { - src += ''; - } - src += '' + optTemplate(options) + ''; - var template = angular.element(src); - el = $compile(template)($rootScope); - $rootScope.$digest(); - }); - attachedElements.push(el); - - return el; - } - - function setup(attrs, options) { - var el; - inject(function($compile, $rootScope) { - var optionsTpl = optTemplate(options); - var fullTpl = '' + optionsTpl + - ''; - el = $compile(fullTpl)($rootScope); - $rootScope.$apply(); - }); - attachedElements.push(el); - - return el; - } - - function setupMultiple(attrs, options) { - attrs = (attrs || '') + ' multiple'; - return setup(attrs, options); - } - - function optTemplate(options) { - var optionsTpl = ''; - inject(function($rootScope) { - if (angular.isArray(options)) { - $rootScope.$$values = options; - optionsTpl = '{{value}}'; - } else if (angular.isString(options)) { - optionsTpl = options; - } + afterEach(inject(function($document) { + attachedElements.forEach(function(element) { + element.remove(); }); - return optionsTpl; - } - - function selectedOptions(el) { - return angular.element(el[0].querySelectorAll('md-option[selected]')); - } - - function openSelect(el) { - try { - el.triggerHandler('click'); - waitForSelectOpen(); - } catch(e) { } - } - - - function pressKey(el, code) { - el.triggerHandler({ - type: 'keydown', - keyCode: code - }); - } - - function waitForSelectOpen() { - try { - inject(function($rootScope, $timeout, $$rAF) { - $rootScope.$digest(); - - $$rAF.flush(); // flush $animate.enter(backdrop) - $timeout.flush(); // flush response - $$rAF.flush(); // flush $animateCss - $timeout.flush(); // flush response - - $rootScope.$digest(); - }); - } catch(e) { } - } - - function waitForSelectClose() { - try { - inject(function($rootScope, $timeout, $$rAF) { - $rootScope.$digest(); - - $$rAF.flush(); // flush $animate.leave(backdrop) - $timeout.flush(); // flush response - - $rootScope.$digest(); + attachedElements = []; - $$rAF.flush(); - $rootScope.$digest(); - }); - } catch(e) { } - } + var selectMenus = $document.find('md-select-menu'); + selectMenus.remove(); + })); it('should preserve tabindex', inject(function($document) { var select = setupSelect('tabindex="2", ng-model="val"').find('md-select'); @@ -138,7 +40,7 @@ describe('', function() { expect(container.classList.contains('test')).toBe(true); })); - it('closes the menu if the element is destroyed', inject(function($document, $rootScope) { + it('closes the menu if the element on backdrop click', inject(function($document, $rootScope) { var called = false; $rootScope.onClose = function() { called = true; @@ -156,6 +58,24 @@ describe('', function() { expect(called).toBe(true); })); + it('closes the menu during scope.$destroy()', inject(function($document, $rootScope, $timeout) { + var container = angular.element("
"); + var scope = $rootScope.$new(); + var select = setupSelect(' ng-model="val" ', [1, 2, 3], false, scope).find('md-select'); + + $document[0].body.appendChild(container[0]); + container.append(select); + + openSelect(select); + + scope.$destroy(); + $rootScope.$digest(); + $timeout.flush(); + + expect($document[0].querySelector("md-select-menu")).toBe(null); + })); + + it('restores focus to select when the menu is closed', inject(function($document) { var select = setupSelect('ng-model="val"').find('md-select'); openSelect(select); @@ -168,8 +88,6 @@ describe('', function() { // FIXME- does not work with minified, jquery //expect($document[0].activeElement).toBe(select[0]); - - select.remove(); })); it('should not convert numbers to strings', inject(function($compile, $rootScope) { @@ -182,11 +100,6 @@ describe('', function() { })); describe('input container', function() { - beforeEach(inject(function($document) { - var selectMenus = $document.find('md-select-menu'); - selectMenus.remove(); - })); - it('should set has-value class on container for non-ng-model input', inject(function($rootScope, $document) { var el = setupSelect('ng-model="$root.model"', [1, 2, 3]); var select = el.find('md-select'); @@ -831,4 +744,105 @@ describe('', function() { })); }); }); + + function setupSelect(attrs, options, skipLabel, scope) { + var el; + + inject(function($compile, $rootScope) { + var src = ''; + if (!skipLabel) { + src += ''; + } + src += '' + optTemplate(options) + ''; + var template = angular.element(src); + el = $compile(template)(scope || $rootScope); + $rootScope.$digest(); + }); + attachedElements.push(el); + + return el; + } + + function setup(attrs, options) { + var el; + inject(function($compile, $rootScope) { + var optionsTpl = optTemplate(options); + var fullTpl = '' + optionsTpl + + ''; + el = $compile(fullTpl)($rootScope); + $rootScope.$apply(); + }); + attachedElements.push(el); + + return el; + } + + function setupMultiple(attrs, options) { + attrs = (attrs || '') + ' multiple'; + return setup(attrs, options); + } + + function optTemplate(options) { + var optionsTpl = ''; + inject(function($rootScope) { + if (angular.isArray(options)) { + $rootScope.$$values = options; + optionsTpl = '{{value}}'; + } else if (angular.isString(options)) { + optionsTpl = options; + } + }); + return optionsTpl; + } + + function selectedOptions(el) { + return angular.element(el[0].querySelectorAll('md-option[selected]')); + } + + function openSelect(el) { + try { + el.triggerHandler('click'); + waitForSelectOpen(); + } catch(e) { } + } + + + function pressKey(el, code) { + el.triggerHandler({ + type: 'keydown', + keyCode: code + }); + } + + function waitForSelectOpen() { + try { + inject(function($rootScope, $timeout, $$rAF) { + $rootScope.$digest(); + + $$rAF.flush(); // flush $animate.enter(backdrop) + $timeout.flush(); // flush response + $$rAF.flush(); // flush $animateCss + $timeout.flush(); // flush response + + $rootScope.$digest(); + }); + } catch(e) { } + } + + function waitForSelectClose() { + try { + inject(function($rootScope, $timeout, $$rAF) { + $rootScope.$digest(); + + $$rAF.flush(); // flush $animate.leave(backdrop) + $timeout.flush(); // flush response + + $rootScope.$digest(); + + $$rAF.flush(); + $rootScope.$digest(); + }); + } catch(e) { } + } + }); diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index d752560a7b7..9f742a47b34 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -11,9 +11,17 @@ angular.module('material.components.toast', [ .directive('mdToast', MdToastDirective) .provider('$mdToast', MdToastProvider); -function MdToastDirective() { +/* @ngInject */ +function MdToastDirective($mdToast) { return { - restrict: 'E' + restrict: 'E', + link: function postLink(scope, element, attr) { + // When navigation force destroys an interimElement, then + // listen and $destroy() that interim instance... + scope.$on('$destroy', function() { + $mdToast.destroy(); + }); + } }; } @@ -254,7 +262,7 @@ function MdToastProvider($$interimElementProvider) { element.off(SWIPE_EVENTS, options.onSwipe); options.parent.removeClass(options.openClass); - return $animate.leave(element); + return (options.$destroy == true) ? element.remove() : $animate.leave(element); } function toastOpenClass(position) { diff --git a/src/core/services/interimElement/interimElement.js b/src/core/services/interimElement/interimElement.js index b12e6c3e83b..2f29b2ccb46 100644 --- a/src/core/services/interimElement/interimElement.js +++ b/src/core/services/interimElement/interimElement.js @@ -116,9 +116,14 @@ function InterimElementProvider() { var publicService = { hide: interimElementService.hide, cancel: interimElementService.cancel, - show: showInterimElement + show: showInterimElement, + + // Special internal method to destroy an interim element without animations + // used when navigation changes causes a $scope.$destroy() action + destroy : destroyInterimElement }; + defaultMethods = providerConfig.methods || []; // This must be invoked after the publicService is initialized defaultOptions = invokeFactory(providerConfig.optionsFactory, {}); @@ -178,9 +183,11 @@ function InterimElementProvider() { // // @example `$mdToast.simple('hello')` // sets options.content to hello // // because argOption === 'content' - if (arguments.length && definition.argOption && !angular.isObject(arg) && - !angular.isArray(arg)) { + if (arguments.length && definition.argOption && + !angular.isObject(arg) && !angular.isArray(arg)) { + return (new Preset())[definition.argOption](arg); + } else { return new Preset(arg); } @@ -190,6 +197,9 @@ function InterimElementProvider() { return publicService; + /** + * + */ function showInterimElement(opts) { // opts is either a preset which stores its options on an _options field, // or just an object made up of options @@ -201,6 +211,17 @@ function InterimElementProvider() { ); } + /** + * Special method to hide and destroy an interimElement WITHOUT + * any 'leave` or hide animations ( an immediate force hide/remove ) + * + * NOTE: This calls the onRemove() subclass method for each component... + * which must have code to respond to `options.$destroy == true` + */ + function destroyInterimElement(opts) { + return interimElementService.destroy(opts); + } + /** * Helper to call $injector.invoke with a local of the factory name for * this provider. @@ -219,7 +240,7 @@ function InterimElementProvider() { } /* @ngInject */ - function InterimElementFactory($document, $q, $rootScope, $timeout, $rootElement, $animate, + function InterimElementFactory($document, $q, $$q, $rootScope, $timeout, $rootElement, $animate, $mdUtil, $mdCompiler, $mdTheming, $log ) { return function createInterimElementService() { var SHOW_CANCELLED = false; @@ -241,7 +262,8 @@ function InterimElementProvider() { return service = { show: show, hide: hide, - cancel: cancel + cancel: cancel, + destroy : destroy }; /* @@ -259,7 +281,7 @@ function InterimElementProvider() { */ function show(options) { options = options || {}; - var interimElement = new InterimElement(options); + var interimElement = new InterimElement(options || {}); var hideExisting = !options.skipHide && stack.length ? service.hide() : $q.when(true); // This hide()s only the current interim element before showing the next, new one @@ -271,7 +293,8 @@ function InterimElementProvider() { interimElement .show() .catch(function( reason ) { - // $log.error("InterimElement.show() error: " + reason ); + //$log.error("InterimElement.show() error: " + reason ); + return reason; }); }); @@ -311,9 +334,10 @@ function InterimElementProvider() { function closeElement(interim) { interim - .remove(reason || SHOW_CLOSED, false) + .remove(reason || SHOW_CLOSED, false, options || { }) .catch(function( reason ) { - // $log.error("InterimElement.hide() error: " + reason ); + //$log.error("InterimElement.hide() error: " + reason ); + return reason; }); return interim.deferred.promise; } @@ -331,19 +355,30 @@ function InterimElementProvider() { * @returns Promise that will be resolved after the element has been removed. * */ - function cancel(reason) { + function cancel(reason, options) { var interim = stack.shift(); if ( !interim ) return $q.when(reason || SHOW_CANCELLED); interim - .remove(reason || SHOW_CANCELLED, true) + .remove(reason || SHOW_CANCELLED, true, options || { }) .catch(function( reason ) { - // $log.error("InterimElement.cancel() error: " + reason ); + //$log.error("InterimElement.cancel() error: " + reason ); + return reason; }); return interim.deferred.promise; } + /* + * Special method to quick-remove the interim element without animations + */ + function destroy() { + var interim = stack.shift(); + + return interim ? interim.remove(SHOW_CANCELLED, false, {'$destroy':true}) : + $q.when(SHOW_CANCELLED); + } + /* * Internal Interim Element Object @@ -393,39 +428,44 @@ function InterimElementProvider() { * - perform the transition-out, and * - perform optional clean up scope. */ - function transitionOutAndRemove(response, isCancelled) { + function transitionOutAndRemove(response, isCancelled, opts) { + options = angular.merge(options || {}, opts || {}); options.cancelAutoHide && options.cancelAutoHide(); + options.element.triggerHandler('$mdInterimElementRemove'); - return $q(function(resolve, reject){ + if ( options.$destroy === true ) { - $q.when(showAction).finally(function(){ - options.element.triggerHandler('$mdInterimElementRemove'); - hideElement(options.element, options).then( function() { + return hideElement(options.element, options); - (isCancelled && rejectAll(response)) || resolveAll(); + } else { - }, rejectAll ); + $q.when(showAction) + .finally(function() { + hideElement(options.element, options).then(function() { - }); + (isCancelled && rejectAll(response)) || resolveAll(response); - function resolveAll() { - // The `show()` returns a promise that will be resolved when the interim - // element is hidden or cancelled... - self.deferred.resolve(response); + }, rejectAll); + }); - // Now resolve the `.hide()` promise itself (optional) - resolve(response); - } + return self.deferred.promise; + } - function rejectAll(fault) { - // Force the '$md.show()' promise to reject - self.deferred.reject(fault); - // Continue rejection propagation - reject(fault); - } + /** + * The `show()` returns a promise that will be resolved when the interim + * element is hidden or cancelled... + */ + function resolveAll(response) { + self.deferred.resolve(response); + } - }); + /** + * Force the '$md.show()' promise to reject + */ + function rejectAll(fault) { + self.deferred.reject(fault); + } } /** @@ -578,21 +618,32 @@ function InterimElementProvider() { function hideElement(element, options) { var announceRemoving = options.onRemoving || angular.noop; - return $q(function (resolve, reject) { + return $$q(function (resolve, reject) { try { // Start transitionIn - var action = $q.when(element ? options.onRemove(options.scope, element, options) : true); + var action = $$q.when( options.onRemove(options.scope, element, options) || true ); // Trigger callback *before* the remove operation starts announceRemoving(element, action); - // Wait until transition-out is done - action.then(function () { + if ( options.$destroy == true ) { - !options.preserveScope && options.scope.$destroy(); + // For $destroy, onRemove should be synchronous resolve(element); - }, reject ); + } else { + + // Wait until transition-out is done + action.then(function () { + + if (!options.preserveScope && options.scope ) { + options.scope.$destroy(); + } + + resolve(element); + + }, reject ); + } } catch(e) { reject(e.message);