From 958d3d56b1899a2cfc7b18c0292e5a1d8c64d0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 4 Dec 2013 12:49:02 -0500 Subject: [PATCH] fix($animate): ensure animations work with directives that share a transclusion Closes #4716 Closes #4871 Closes #5021 Closes #5278 --- src/ngAnimate/animate.js | 61 ++++++++++++++++++++++++----------- test/ngAnimate/animateSpec.js | 53 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 18796ba9f216..8ff7b4298725 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -258,6 +258,19 @@ angular.module('ngAnimate', ['ng']) var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running: true}; + function extractElementNode(element) { + for(var i = 0; i < element.length; i++) { + var elm = element[i]; + if(elm.nodeType == ELEMENT_NODE) { + return elm; + } + } + } + + function isMatchingElement(elm1, elm2) { + return extractElementNode(elm1) == extractElementNode(elm2); + } + $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document', function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) { @@ -556,7 +569,16 @@ angular.module('ngAnimate', ['ng']) and the onComplete callback will be fired once the animation is fully complete. */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - var currentClassName = element.attr('class') || ''; + var node = extractElementNode(element); + //transcluded directives may sometimes fire an animation using only comment nodes + //best to catch this early on to prevent any animation operations from occurring + if(!node) { + fireDOMOperation(); + closeAnimation(); + return; + } + + var currentClassName = node.className; var classes = currentClassName + ' ' + className; var animationLookup = (' ' + classes).replace(/\s+/g,'.'); if (!parentElement) { @@ -760,11 +782,7 @@ angular.module('ngAnimate', ['ng']) } function cancelChildAnimations(element) { - var node = element[0]; - if(node.nodeType != ELEMENT_NODE) { - return; - } - + var node = extractElementNode(element); forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) { element = angular.element(element); var data = element.data(NG_ANIMATE_STATE); @@ -788,7 +806,7 @@ angular.module('ngAnimate', ['ng']) } function cleanup(element) { - if(element[0] == $rootElement[0]) { + if(isMatchingElement(element, $rootElement)) { if(!rootAnimateState.disabled) { rootAnimateState.running = false; rootAnimateState.structural = false; @@ -802,7 +820,7 @@ angular.module('ngAnimate', ['ng']) function animationsDisabled(element, parentElement) { if (rootAnimateState.disabled) return true; - if(element[0] == $rootElement[0]) { + if(isMatchingElement(element, $rootElement)) { return rootAnimateState.disabled || rootAnimateState.running; } @@ -812,7 +830,7 @@ angular.module('ngAnimate', ['ng']) //any animations on it if(parentElement.length === 0) break; - var isRoot = parentElement[0] == $rootElement[0]; + var isRoot = isMatchingElement(parentElement, $rootElement); var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); var result = state && (!!state.disabled || !!state.running); if(isRoot || result) { @@ -960,7 +978,7 @@ angular.module('ngAnimate', ['ng']) parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } - return parentID + '-' + element[0].className; + return parentID + '-' + extractElementNode(element).className; } function animateSetup(element, className) { @@ -995,7 +1013,6 @@ angular.module('ngAnimate', ['ng']) return false; } - var node = element[0]; //temporarily disable the transition so that the enter styles //don't animate twice (this is here to avoid a bug in Chrome/FF). var activeClassName = ''; @@ -1025,35 +1042,37 @@ angular.module('ngAnimate', ['ng']) } function blockTransitions(element) { - element[0].style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; + extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; } function blockKeyframeAnimations(element) { - element[0].style[ANIMATION_PROP] = 'none 0s'; + extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; } function unblockTransitions(element) { - var node = element[0], prop = TRANSITION_PROP + PROPERTY_KEY; + var prop = TRANSITION_PROP + PROPERTY_KEY; + var node = extractElementNode(element); if(node.style[prop] && node.style[prop].length > 0) { node.style[prop] = ''; } } function unblockKeyframeAnimations(element) { - var node = element[0], prop = ANIMATION_PROP; + var prop = ANIMATION_PROP; + var node = extractElementNode(element); if(node.style[prop] && node.style[prop].length > 0) { - element[0].style[prop] = ''; + node.style[prop] = ''; } } function animateRun(element, className, activeAnimationComplete) { var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(!element.hasClass(className) || !data) { + var node = extractElementNode(element); + if(node.className.indexOf(className) == -1 || !data) { activeAnimationComplete(); return; } - var node = element[0]; var timings = data.timings; var stagger = data.stagger; var maxDuration = data.maxDuration; @@ -1096,6 +1115,9 @@ angular.module('ngAnimate', ['ng']) } if(appliedStyles.length > 0) { + //the element being animated may sometimes contain comment nodes in + //the jqLite object, so we're safe to use a single variable to house + //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; node.setAttribute('style', oldStyle + ' ' + style); } @@ -1110,6 +1132,7 @@ angular.module('ngAnimate', ['ng']) element.off(css3AnimationEvents, onAnimationProgress); element.removeClass(activeClassName); animateClose(element, className); + var node = extractElementNode(element); for (var i in appliedStyles) { node.style.removeProperty(appliedStyles[i]); } @@ -1209,7 +1232,7 @@ angular.module('ngAnimate', ['ng']) } var parentElement = element.parent(); - var clone = angular.element(element[0].cloneNode()); + var clone = angular.element(extractElementNode(element).cloneNode()); //make the element super hidden and override any CSS style values clone.attr('style','position:absolute; top:-9999px; left:-9999px'); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index b3068470e3ed..44b623b43ef1 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -2873,5 +2873,58 @@ describe("ngAnimate", function() { expect($rootElement.children().length).toBe(0); })); + + it('should properly animate elements with compound directives', function() { + var capturedAnimation; + module(function($animateProvider) { + $animateProvider.register('.special', function() { + return { + enter : function(element, done) { + capturedAnimation = 'enter'; + done(); + }, + leave : function(element, done) { + capturedAnimation = 'leave'; + done(); + } + } + }); + }); + inject(function($rootScope, $compile, $rootElement, $document, $timeout, $templateCache, $sniffer) { + if(!$sniffer.transitions) return; + + $templateCache.put('item-template', 'item: #{{ item }} '); + var element = $compile('
' + + '
' + + '
')($rootScope); + + ss.addRule('.special', '-webkit-transition:1s linear all;' + + 'transition:1s linear all;'); + + $rootElement.append(element); + jqLite($document[0].body).append($rootElement); + + $rootScope.tpl = 'item-template'; + $rootScope.items = [1,2,3]; + $rootScope.$digest(); + $timeout.flush(); + + expect(capturedAnimation).toBe('enter'); + expect(element.text()).toContain('item: #1'); + + forEach(element.children(), function(kid) { + browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + }); + $timeout.flush(); + + $rootScope.items = []; + $rootScope.$digest(); + $timeout.flush(); + + expect(capturedAnimation).toBe('leave'); + }); + }); }); });