From b799875891bb7bfd09cf55f96c5ea7a6b3166531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 27 Feb 2014 11:21:50 -0500 Subject: [PATCH] fix($animate): delegate down to addClass/removeClass if setClass is not found Closes #6463 --- src/ngAnimate/animate.js | 272 ++++++++++++++-------------------- test/ngAnimate/animateSpec.js | 106 ++++++++++++- 2 files changed, 214 insertions(+), 164 deletions(-) diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 6ad05ae855ff..6b935a998d34 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -348,13 +348,15 @@ angular.module('ngAnimate', ['ng']) } function animationRunner(element, animationEvent, className) { + //transcluded directives may sometimes fire an animation using only comment nodes + //best to catch this early on to prevent any animation operations from occurring var node = element[0]; if(!node) { - throw new Error; + throw new Error(); } - var setClassOperation = animationEvent == 'setClass'; - var isClassBased = setClassOperation || + var isSetClassOperation = animationEvent == 'setClass'; + var isClassBased = isSetClassOperation || animationEvent == 'addClass' || animationEvent == 'removeClass'; @@ -367,22 +369,21 @@ angular.module('ngAnimate', ['ng']) var currentClassName = element.attr('class'); var classes = currentClassName + ' ' + className; - if(isAnimatableClassName(classes)) { - throw new Error; + if(!isAnimatableClassName(classes)) { + throw new Error(); } - var detectedAnimations = lookup(classes); - - var cancellations, + var beforeComplete = noop, + beforeCancel = [], before = [], + afterComplete = noop, + afterCancel = [], after = []; var animationLookup = (' ' + classes).replace(/\s+/g,'.'); - var matches = lookup(animationLookup); - for(var i = 0; i < matches.length; i++) { - var animationFactory = matches[i]; + forEach(lookup(animationLookup), function(animationFactory) { var created = registerAnimation(animationFactory, animationEvent); - if(!created && setClassOperation) { + if(!created && isSetClassOperation) { registerAnimation(animationFactory, 'addClass'); registerAnimation(animationFactory, 'removeClass'); } @@ -390,73 +391,100 @@ angular.module('ngAnimate', ['ng']) function registerAnimation(animationFactory, event) { var afterFn = animationFactory[event]; - var beforeFn = animationFactory[ - 'before' + animationEvent.charAt(0).toUpperCase() + animationEvent.substr(1)]; + var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)]; if(afterFn || beforeFn) { if(event == 'leave') { beforeFn = afterFn; - afterFn = angular.noop; + //when set as null then animation knows to skip this phase + afterFn = null; } after.push({ - event : event, - fn : afterFn || angular.noop - }); + event : event, fn : afterFn + }); before.push({ - event : event, - fn : beforeFn || angular.noop + event : event, fn : beforeFn }); return true; } - }; - - function run(animations, onAllComplete) { - cancellations = []; - var count = 0, total = animations.length; - - function onComplete() { - if(!cancellations) return; + } - cancellations[i](); - if(++count < total) return; + function run(fns, cancellations, allCompleteFn) { + var animations = []; + angular.forEach(fns, function(animation) { + animation.fn && animations.push(animation); + }); - cancellations = null; - onAllComplete(); - }; + var count = 0; + function afterAnimationComplete(index) { + if(cancellations) { + (cancellations[index] || noop)(); + if(++count < animations.length) return; + cancellations = null; + } + allCompleteFn(); + } - angular.forEach(animations, function(animation) { - var cancelFn; + //The code below adds directly to the array in order to work with + //both sync and async animations. Sync animations are when the done() + //operation is called right away. DO NOT REFACTOR! + angular.forEach(animations, function(animation, index) { + var progress = function() { + afterAnimationComplete(index); + }; switch(animation.event) { case 'setClass': - cancelFn = animation.fn(element, classNameAdd, classNameRemove, done); + cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress)); break; case 'addClass': + cancellations.push(animation.fn(element, classNameAdd || className, progress)); + break; case 'removeClass': - cancelFn = animation.fn(element, className, done); + cancellations.push(animation.fn(element, classNameRemove || className, progress)); break; default: - cancelFn = animation.fn(element, done); + cancellations.push(animation.fn(element, progress)); break; } - cancellations.push(cancelFn || angular.noop); }); + + if(cancellations && cancellations.length === 0) { + allCompleteFn(); + } } return { - isClassBased : - allowAnimations : function() { - return true; - }, + node : node, + event : animationEvent, + className : className, + isClassBased : isClassBased, + isSetClassOperation : isSetClassOperation, before : function(allCompleteFn) { - run(before, allCompleteFn); + beforeComplete = allCompleteFn; + run(before, beforeCancel, function() { + beforeComplete = noop; + allCompleteFn(); + }); }, after : function(allCompleteFn) { - run(before, allCompleteFn); - }, - cancelAnmations : function() { - angular.forEach(cancellation, function(cancelFn) { - cancelFn(true); + afterComplete = allCompleteFn; + run(after, afterCancel, function() { + afterComplete = noop; + allCompleteFn(); }); - cancellations = null; + }, + cancel : function() { + if(beforeCancel) { + angular.forEach(beforeCancel, function(cancelFn) { + (cancelFn || noop)(true); + }); + beforeComplete(true); + } + if(afterCancel) { + angular.forEach(afterCancel, function(cancelFn) { + (cancelFn || noop)(true); + }); + afterComplete(true); + } } }; } @@ -736,8 +764,6 @@ angular.module('ngAnimate', ['ng']) */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, doneCallback) { - //transcluded directives may sometimes fire an animation using only comment nodes - //best to catch this early on to prevent any animation operations from occurring var runner; try { runner = animationRunner(element, animationEvent, className); @@ -749,7 +775,8 @@ angular.module('ngAnimate', ['ng']) return; } - var elementEvents = angular.element._data(node); + className = runner.className; + var elementEvents = angular.element._data(runner.node); elementEvents = elementEvents && elementEvents.events; if (!parentElement) { @@ -757,7 +784,6 @@ angular.module('ngAnimate', ['ng']) } var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; var totalActiveAnimations = ngAnimateState.totalActive || 0; var lastAnimation = ngAnimateState.last; @@ -765,16 +791,14 @@ angular.module('ngAnimate', ['ng']) //only allow animations if the currently running animation is not structural //or if there is no animation running at all var skipAnimations = runner.isClassBased ? - !ngAnimateState.disabled && (!lastAnimation || lastAnimation.classBased) : - true; + ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) : + false; //skip the animation if animations are disabled, a parent is already being animated, //the element is not currently attached to the document body or then completely close //the animation if any matching animations are not found at all. - //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found. - if (skipAnimations || - !runner.allowAnimations() || - animationsDisabled(element, parentElement)) { + //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found. + if (skipAnimations || animationsDisabled(element, parentElement)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); @@ -785,7 +809,7 @@ angular.module('ngAnimate', ['ng']) var skipAnimation = false; if(totalActiveAnimations > 0) { var animationsToCancel = []; - if(!isClassBased) { + if(!runner.isClassBased) { if(animationEvent == 'leave' && runningAnimations['ng-leave']) { skipAnimation = true; } else { @@ -813,13 +837,12 @@ angular.module('ngAnimate', ['ng']) if(animationsToCancel.length > 0) { angular.forEach(animationsToCancel, function(operation) { - (operation.done || noop)(true); - cancelAnimations(operation.animations); + operation.cancel(); }); } } - if(isClassBased && !setClassOperation && !skipAnimation) { + if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } @@ -836,15 +859,11 @@ angular.module('ngAnimate', ['ng']) //is cancelled midway element.one('$destroy', function(e) { var element = angular.element(this); - var state = element.data(NG_ANIMATE_STATE) || {}; - var activeLeaveAnimation = state.active['ng-leave']; - if(activeLeaveAnimation) { - var animations = activeLeaveAnimation.animations; - - //if the before animation is completed then the element will be - //removed shortly after so there is no need to cancel the animation - if(!animations[0].beforeComplete) { - cancelAnimations(animations); + var state = element.data(NG_ANIMATE_STATE); + if(state) { + var activeLeaveAnimation = state.active['ng-leave']; + if(activeLeaveAnimation) { + activeLeaveAnimation.cancel(); cleanup(element, 'ng-leave'); } } @@ -856,18 +875,11 @@ angular.module('ngAnimate', ['ng']) element.addClass(NG_ANIMATE_CLASS_NAME); var localAnimationCount = globalAnimationCounter++; - lastAnimation = { - classBased : isClassBased, - event : animationEvent, - animations : animations, - done:onBeforeAnimationsComplete - }; - totalActiveAnimations++; - runningAnimations[className] = lastAnimation; + runningAnimations[className] = runner; element.data(NG_ANIMATE_STATE, { - last : lastAnimation, + last : runner, active : runningAnimations, index : localAnimationCount, totalActive : totalActiveAnimations @@ -875,72 +887,21 @@ angular.module('ngAnimate', ['ng']) //first we run the before animations and when all of those are complete //then we perform the DOM operation and run the next set of animations - invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete); - - function onBeforeAnimationsComplete(cancelled) { + fireBeforeCallbackAsync(); + runner.before(function(cancelled) { var data = element.data(NG_ANIMATE_STATE); cancelled = cancelled || - !data || !data.active[className] || - (isClassBased && data.active[className].event != animationEvent); + !data || !data.active[className] || + (runner.isClassBased && data.active[className].event != animationEvent); fireDOMOperation(); if(cancelled === true) { closeAnimation(); - return; + } else { + fireAfterCallbackAsync(); + runner.after(closeAnimation); } - - //set the done function to the final done function - //so that the DOM event won't be executed twice by accident - //if the after animation is cancelled as well - var currentAnimation = data.active[className]; - currentAnimation.done = closeAnimation; - invokeRegisteredAnimationFns(animations, 'after', closeAnimation); - } - - function invokeRegisteredAnimationFns(animations, phase, allAnimationFnsComplete) { - phase == 'after' ? - fireAfterCallbackAsync() : - fireBeforeCallbackAsync(); - - var endFnName = phase + 'End'; - forEach(animations, function(animation, index) { - var animationPhaseCompleted = function() { - progress(index, phase); - }; - - //there are no before functions for enter + move since the DOM - //operations happen before the performAnimation method fires - if(phase == 'before' && (animationEvent == 'enter' || animationEvent == 'move')) { - animationPhaseCompleted(); - return; - } - - if(animation[phase]) { - if(setClassOperation) { - animation[endFnName] = animation[phase](element, classNameAdd, classNameRemove, animationPhaseCompleted); - } else { - animation[endFnName] = isClassBased ? - animation[phase](element, className, animationPhaseCompleted) : - animation[phase](element, animationPhaseCompleted); - } - } else { - animationPhaseCompleted(); - } - }); - - function progress(index, phase) { - var phaseCompletionFlag = phase + 'Complete'; - var currentAnimation = animations[index]; - currentAnimation[phaseCompletionFlag] = true; - (currentAnimation[endFnName] || noop)(); - - for(var i=0;i'); + var element = parent.find('span'); + $rootElement.append(parent); + body.append($rootElement); + + expect(element.hasClass('on')).toBe(false); + expect(element.hasClass('off')).toBe(true); + + var signature = ''; + $animate.setClass(element, 'on', 'off', function() { + signature += 'Z'; + }); + + $animate.triggerCallbacks(); + + expect(signature).toBe('Z'); + expect(element.hasClass('on')).toBe(true); + expect(element.hasClass('off')).toBe(false); + })); + it('should fire DOM callbacks on the element being animated', inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) {