diff options
| -rw-r--r-- | css/angular.css | 5 | ||||
| -rw-r--r-- | src/ng/animate.js | 23 | ||||
| -rw-r--r-- | src/ng/compile.js | 12 | ||||
| -rw-r--r-- | src/ngAnimate/animate.js | 491 | ||||
| -rw-r--r-- | src/ngMock/angular-mocks.js | 3 | ||||
| -rwxr-xr-x | test/ng/compileSpec.js | 6 | ||||
| -rw-r--r-- | test/ng/directive/ngClassSpec.js | 10 | ||||
| -rw-r--r-- | test/ngAnimate/animateSpec.js | 71 | ||||
| -rw-r--r-- | test/ngRoute/directive/ngViewSpec.js | 3 | 
9 files changed, 344 insertions, 280 deletions
| diff --git a/css/angular.css b/css/angular.css index b88e61e4..2566640e 100644 --- a/css/angular.css +++ b/css/angular.css @@ -9,3 +9,8 @@  ng\:form {    display: block;  } + +.ng-animate-block-transitions { +  transition:0s all!important; +  -webkit-transition:0s all!important; +} diff --git a/src/ng/animate.js b/src/ng/animate.js index fa5b936d..1961d47b 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -222,6 +222,29 @@ var $AnimateProvider = ['$provide', function($provide) {          done && $timeout(done, 0, false);        }, +      /** +       * +       * @ngdoc function +       * @name ng.$animate#setClass +       * @methodOf ng.$animate +       * @function +       * @description Adds and/or removes the given CSS classes to and from the element. +       * Once complete, the done() callback will be fired (if provided). +       * @param {jQuery/jqLite element} element the element which will it's CSS classes changed +       *   removed from it +       * @param {string} add the CSS classes which will be added to the element +       * @param {string} remove the CSS class which will be removed from the element +       * @param {function=} done the callback function (if provided) that will be fired after the +       *   CSS classes have been set on the element +       */ +      setClass : function(element, add, remove, done) { +        forEach(element, function (element) { +          jqLiteAddClass(element, add); +          jqLiteRemoveClass(element, remove); +        }); +        done && $timeout(done, 0, false); +      }, +        enabled : noop      };    }]; diff --git a/src/ng/compile.js b/src/ng/compile.js index e4bf230e..ded62ea9 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -690,8 +690,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {         * @param {string} oldClasses The former CSS className value         */        $updateClass : function(newClasses, oldClasses) { -        this.$removeClass(tokenDifference(oldClasses, newClasses)); -        this.$addClass(tokenDifference(newClasses, oldClasses)); +        var toAdd = tokenDifference(newClasses, oldClasses); +        var toRemove = tokenDifference(oldClasses, newClasses); + +        if(toAdd.length === 0) { +          $animate.removeClass(this.$$element, toRemove); +        } else if(toRemove.length === 0) { +          $animate.addClass(this.$$element, toAdd); +        } else { +          $animate.setClass(this.$$element, toAdd, toRemove); +        }        },        /** diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index dba8d3fa..28a1510a 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -248,7 +248,9 @@ angular.module('ngAnimate', ['ng'])     * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.     *     */ -  .factory('$$animateReflow', ['$window', '$timeout', function($window, $timeout) { +  .factory('$$animateReflow', ['$window', '$timeout', '$document', +                       function($window,   $timeout,   $document) { +    var bod = $document[0].body;      var requestAnimationFrame = $window.requestAnimationFrame       ||                                  $window.webkitRequestAnimationFrame ||                                  function(fn) { @@ -261,7 +263,10 @@ angular.module('ngAnimate', ['ng'])                                   return $timeout.cancel(timer);                                 };      return function(fn) { -      var id = requestAnimationFrame(fn); +      var id = requestAnimationFrame(function() { +        var a = bod.offsetWidth + 1; +        fn(); +      });        return function() {          cancelAnimationFrame(id);        }; @@ -301,6 +306,10 @@ angular.module('ngAnimate', ['ng'])        }      } +    function stripCommentsFromElement(element) { +      return angular.element(extractElementNode(element)); +    } +      function isMatchingElement(elm1, elm2) {        return extractElementNode(elm1) == extractElementNode(elm2);      } @@ -411,6 +420,7 @@ angular.module('ngAnimate', ['ng'])            this.enabled(false, element);            $delegate.enter(element, parentElement, afterElement);            $rootScope.$$postDigest(function() { +            element = stripCommentsFromElement(element);              performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback);            });          }, @@ -447,6 +457,7 @@ angular.module('ngAnimate', ['ng'])            cancelChildAnimations(element);            this.enabled(false, element);            $rootScope.$$postDigest(function() { +            element = stripCommentsFromElement(element);              performAnimation('leave', 'ng-leave', element, null, null, function() {                $delegate.leave(element);              }, doneCallback); @@ -489,6 +500,7 @@ angular.module('ngAnimate', ['ng'])            this.enabled(false, element);            $delegate.move(element, parentElement, afterElement);            $rootScope.$$postDigest(function() { +            element = stripCommentsFromElement(element);              performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback);            });          }, @@ -524,6 +536,7 @@ angular.module('ngAnimate', ['ng'])           * @param {function()=} doneCallback the callback function that will be called once the animation is complete          */          addClass : function(element, className, doneCallback) { +          element = stripCommentsFromElement(element);            performAnimation('addClass', className, element, null, null, function() {              $delegate.addClass(element, className);            }, doneCallback); @@ -560,11 +573,34 @@ angular.module('ngAnimate', ['ng'])           * @param {function()=} doneCallback the callback function that will be called once the animation is complete          */          removeClass : function(element, className, doneCallback) { +          element = stripCommentsFromElement(element);            performAnimation('removeClass', className, element, null, null, function() {              $delegate.removeClass(element, className);            }, doneCallback);          }, +          /** +           * +           * @ngdoc function +           * @name ng.$animate#setClass +           * @methodOf ng.$animate +           * @function +           * @description Adds and/or removes the given CSS classes to and from the element. +           * Once complete, the done() callback will be fired (if provided). +           * @param {jQuery/jqLite element} element the element which will it's CSS classes changed +           *   removed from it +           * @param {string} add the CSS classes which will be added to the element +           * @param {string} remove the CSS class which will be removed from the element +           * @param {function=} done the callback function (if provided) that will be fired after the +           *   CSS classes have been set on the element +           */ +        setClass : function(element, add, remove, doneCallback) { +          element = stripCommentsFromElement(element); +          performAnimation('setClass', [add, remove], element, null, null, function() { +            $delegate.setClass(element, add, remove); +          }, doneCallback); +        }, +          /**           * @ngdoc function           * @name ngAnimate.$animate#enabled @@ -611,7 +647,15 @@ 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, classes, node = extractElementNode(element); + +        var classNameAdd, classNameRemove, setClassOperation = animationEvent == 'setClass'; +        if(setClassOperation) { +          classNameAdd = className[0]; +          classNameRemove = className[1]; +          className = classNameAdd + ' ' + classNameRemove; +        } + +        var currentClassName, classes, node = element[0];          if(node) {            currentClassName = node.className;            classes = currentClassName + ' ' + className; @@ -623,7 +667,7 @@ angular.module('ngAnimate', ['ng'])            fireDOMOperation();            fireBeforeCallbackAsync();            fireAfterCallbackAsync(); -          closeAnimation(); +          fireDoneCallbackAsync();            return;          } @@ -635,9 +679,15 @@ angular.module('ngAnimate', ['ng'])            parentElement = afterElement ? afterElement.parent() : element.parent();          } -        var matches = lookup(animationLookup); -        var isClassBased = animationEvent == 'addClass' || animationEvent == 'removeClass'; -        var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; +        var matches         = lookup(animationLookup); +        var isClassBased    = animationEvent == 'addClass' || +                              animationEvent == 'removeClass' || +                              setClassOperation; +        var ngAnimateState  = element.data(NG_ANIMATE_STATE) || {}; + +        var runningAnimations     = ngAnimateState.active || {}; +        var totalActiveAnimations = ngAnimateState.totalActive || 0; +        var lastAnimation         = ngAnimateState.last;          //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 @@ -656,7 +706,7 @@ angular.module('ngAnimate', ['ng'])          //only add animations if the currently running animation is not structural          //or if there is no animation running at all          var allowAnimations = isClassBased ? -          !ngAnimateState.disabled && (!ngAnimateState.running || !ngAnimateState.structural) : +          !ngAnimateState.disabled && (!lastAnimation || lastAnimation.classBased) :            true;          if(allowAnimations) { @@ -691,54 +741,48 @@ angular.module('ngAnimate', ['ng'])            return;          } -        var ONE_SPACE = ' '; -        //this value will be searched for class-based CSS className lookup. Therefore, -        //we prefix and suffix the current className value with spaces to avoid substring -        //lookups of className tokens -        var futureClassName = ONE_SPACE + currentClassName + ONE_SPACE; -        if(ngAnimateState.running) { -          //if an animation is currently running on the element then lets take the steps -          //to cancel that animation and fire any required callbacks -          cleanup(element); -          cancelAnimations(ngAnimateState.animations); - -          //in the event that the CSS is class is quickly added and removed back -          //then we don't want to wait until after the reflow to add/remove the CSS -          //class since both class animations may run into a race condition. -          //The code below will check to see if that is occurring and will -          //immediately remove the former class before the reflow so that the -          //animation can snap back to the original animation smoothly -          var isFullyClassBasedAnimation = isClassBased && !ngAnimateState.structural; -          var isRevertingClassAnimation = isFullyClassBasedAnimation && -                                          ngAnimateState.className == className && -                                          animationEvent != ngAnimateState.event; - -          //if the class is removed during the reflow then it will revert the styles temporarily -          //back to the base class CSS styling causing a jump-like effect to occur. This check -          //here ensures that the domOperation is only performed after the reflow has commenced -          if(ngAnimateState.beforeComplete || isRevertingClassAnimation) { -            (ngAnimateState.done || noop)(true); -          } else if(isFullyClassBasedAnimation) { -            //class-based animations will compare element className values after cancelling the -            //previous animation to see if the element properties already contain the final CSS -            //class and if so then the animation will be skipped. Since the domOperation will -            //be performed only after the reflow is complete then our element's className value -            //will be invalid. Therefore the same string manipulation that would occur within the -            //DOM operation will be performed below so that the class comparison is valid... -            futureClassName = ngAnimateState.event == 'removeClass' ? -              futureClassName.replace(ONE_SPACE + ngAnimateState.className + ONE_SPACE, ONE_SPACE) : -              futureClassName + ngAnimateState.className + ONE_SPACE; +        var skipAnimation = false; +        if(totalActiveAnimations > 0) { +          var animationsToCancel = []; +          if(!isClassBased) { +            if(animationEvent == 'leave' && runningAnimations['ng-leave']) { +              skipAnimation = true; +            } else { +              //cancel all animations when a structural animation takes place +              for(var klass in runningAnimations) { +                animationsToCancel.push(runningAnimations[klass]); +                cleanup(element, klass); +              } +              runningAnimations = {}; +              totalActiveAnimations = 0; +            } +          } else if(lastAnimation.event == 'setClass') { +            animationsToCancel.push(lastAnimation); +            cleanup(element, className); +          } +          else if(runningAnimations[className]) { +            var current = runningAnimations[className]; +            if(current.event == animationEvent) { +              skipAnimation = true; +            } else { +              animationsToCancel.push(current); +              cleanup(element, className); +            } +          } + +          if(animationsToCancel.length > 0) { +            angular.forEach(animationsToCancel, function(operation) { +              (operation.done || noop)(true); +              cancelAnimations(operation.animations); +            });            }          } -        //There is no point in perform a class-based animation if the element already contains -        //(on addClass) or doesn't contain (on removeClass) the className being animated. -        //The reason why this is being called after the previous animations are cancelled -        //is so that the CSS classes present on the element can be properly examined. -        var classNameToken = ONE_SPACE + className + ONE_SPACE; -        if((animationEvent == 'addClass'    && futureClassName.indexOf(classNameToken) >= 0) || -           (animationEvent == 'removeClass' && futureClassName.indexOf(classNameToken) == -1)) { -          fireDOMOperation(); +        if(isClassBased && !setClassOperation && !skipAnimation) { +          skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR +        } + +        if(skipAnimation) {            fireBeforeCallbackAsync();            fireAfterCallbackAsync();            fireDoneCallbackAsync(); @@ -750,15 +794,21 @@ 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;          element.data(NG_ANIMATE_STATE, { -          running:true, -          event:animationEvent, -          className:className, -          structural:!isClassBased, -          animations:animations, -          index:localAnimationCount, -          done:onBeforeAnimationsComplete +          last : lastAnimation, +          active : runningAnimations, +          index : localAnimationCount, +          totalActive : totalActiveAnimations          });          //first we run the before animations and when all of those are complete @@ -766,6 +816,11 @@ angular.module('ngAnimate', ['ng'])          invokeRegisteredAnimationFns(animations, 'before', onBeforeAnimationsComplete);          function onBeforeAnimationsComplete(cancelled) { +          var data = element.data(NG_ANIMATE_STATE); +          cancelled = cancelled || +                      !data || !data.active[className] || +                      (isClassBased && data.active[className].event != animationEvent); +            fireDOMOperation();            if(cancelled === true) {              closeAnimation(); @@ -775,11 +830,8 @@ angular.module('ngAnimate', ['ng'])            //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 data = element.data(NG_ANIMATE_STATE); -          if(data) { -            data.done = closeAnimation; -            element.data(NG_ANIMATE_STATE, data); -          } +          var currentAnimation = data.active[className]; +          currentAnimation.done = closeAnimation;            invokeRegisteredAnimationFns(animations, 'after', closeAnimation);          } @@ -802,9 +854,13 @@ angular.module('ngAnimate', ['ng'])              }              if(animation[phase]) { -              animation[endFnName] = isClassBased ? -                animation[phase](element, className, animationPhaseCompleted) : -                animation[phase](element, animationPhaseCompleted); +              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();              } @@ -872,12 +928,12 @@ angular.module('ngAnimate', ['ng'])                   failing would be when a parent HTML tag has a ng-class attribute                   causing ALL directives below to skip animations during the digest */                if(isClassBased) { -                cleanup(element); +                cleanup(element, className);                } else {                  $$asyncQueueBuffer(function() {                    var data = element.data(NG_ANIMATE_STATE) || {};                    if(localAnimationCount == data.index) { -                    cleanup(element); +                    cleanup(element, className, animationEvent);                    }                  });                  element.data(NG_ANIMATE_STATE, data); @@ -893,9 +949,11 @@ angular.module('ngAnimate', ['ng'])          forEach(node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME), function(element) {            element = angular.element(element);            var data = element.data(NG_ANIMATE_STATE); -          if(data) { -            cancelAnimations(data.animations); -            cleanup(element); +          if(data && data.active) { +            angular.forEach(data.active, function(operation) { +              (operation.done || noop)(true); +              cancelAnimations(operation.animations); +            });            }          });        } @@ -912,15 +970,27 @@ angular.module('ngAnimate', ['ng'])          });        } -      function cleanup(element) { +      function cleanup(element, className) {          if(isMatchingElement(element, $rootElement)) {            if(!rootAnimateState.disabled) {              rootAnimateState.running = false;              rootAnimateState.structural = false;            } -        } else { -          element.removeClass(NG_ANIMATE_CLASS_NAME); -          element.removeData(NG_ANIMATE_STATE); +        } else if(className) { +          var data = element.data(NG_ANIMATE_STATE) || {}; + +          var removeAnimations = className === true; +          if(!removeAnimations) { +            if(data.active && data.active[className]) { +              data.totalActive--; +              delete data.active[className]; +            } +          } + +          if(removeAnimations || !data.totalActive) { +            element.removeClass(NG_ANIMATE_CLASS_NAME); +            element.removeData(NG_ANIMATE_STATE); +          }          }        } @@ -939,7 +1009,7 @@ angular.module('ngAnimate', ['ng'])            var isRoot = isMatchingElement(parentElement, $rootElement);            var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); -          var result = state && (!!state.disabled || !!state.running); +          var result = state && (!!state.disabled || state.running || state.totalActive > 0);            if(isRoot || result) {              return result;            } @@ -989,74 +1059,57 @@ angular.module('ngAnimate', ['ng'])        var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';        var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';        var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; +      var NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions';        var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;        var CLOSING_TIME_BUFFER = 1.5;        var ONE_SECOND = 1000; -      var animationCounter = 0;        var lookupCache = {};        var parentCounter = 0;        var animationReflowQueue = []; -      var animationElementQueue = [];        var cancelAnimationReflow; -      var closingAnimationTime = 0; -      var timeOut = false;        function afterReflow(element, callback) {          if(cancelAnimationReflow) {            cancelAnimationReflow();          } -          animationReflowQueue.push(callback); - -        var node = extractElementNode(element); -        element = angular.element(node); -        animationElementQueue.push(element); - -        var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - -        var stagger = elementData.stagger; -        var staggerTime = elementData.itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); - -        var animationTime = (elementData.maxDelay + elementData.maxDuration) * CLOSING_TIME_BUFFER; -        closingAnimationTime = Math.max(closingAnimationTime, (staggerTime + animationTime) * ONE_SECOND); - -        //by placing a counter we can avoid an accidental -        //race condition which may close an animation when -        //a follow-up animation is midway in its animation -        elementData.animationCount = animationCounter; -          cancelAnimationReflow = $$animateReflow(function() {            forEach(animationReflowQueue, function(fn) {              fn();            }); -          //copy the list of elements so that successive -          //animations won't conflict if they're added before -          //the closing animation timeout has run -          var elementQueueSnapshot = []; -          var animationCounterSnapshot = animationCounter; -          forEach(animationElementQueue, function(elm) { -            elementQueueSnapshot.push(elm); -          }); - -          $timeout(function() { -            closeAllAnimations(elementQueueSnapshot, animationCounterSnapshot); -            elementQueueSnapshot = null; -          }, closingAnimationTime, false); -            animationReflowQueue = []; -          animationElementQueue = [];            cancelAnimationReflow = null;            lookupCache = {}; -          closingAnimationTime = 0; -          animationCounter++;          });        } -      function closeAllAnimations(elements, count) { +      var closingTimer = null; +      var closingTimestamp = 0; +      var animationElementQueue = []; +      function animationCloseHandler(element, totalTime) { +        var futureTimestamp = Date.now() + (totalTime * 1000); +        if(futureTimestamp <= closingTimestamp) { +          return; +        } + +        $timeout.cancel(closingTimer); + +        var node = extractElementNode(element); +        element = angular.element(node); +        animationElementQueue.push(element); + +        closingTimestamp = futureTimestamp; +        closingTimer = $timeout(function() { +          closeAllAnimations(animationElementQueue); +          animationElementQueue = []; +        }, totalTime, false); +      } + +      function closeAllAnimations(elements) {          forEach(elements, function(element) {            var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); -          if(elementData && elementData.animationCount == count) { +          if(elementData) {              (elementData.closeAnimationFn || noop)();            }          }); @@ -1141,12 +1194,12 @@ angular.module('ngAnimate', ['ng'])          return parentID + '-' + extractElementNode(element).className;        } -      function animateSetup(element, className, calculationDecorator) { +      function animateSetup(animationEvent, element, className, calculationDecorator) {          var cacheKey = getCacheKey(element);          var eventCacheKey = cacheKey + ' ' + className; -        var stagger = {};          var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; +        var stagger = {};          if(itemIndex > 0) {            var staggerClassName = className + '-stagger';            var staggerCacheKey = cacheKey + ' ' + staggerClassName; @@ -1166,60 +1219,63 @@ angular.module('ngAnimate', ['ng'])          element.addClass(className); +        var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; +          var timings = calculationDecorator(function() {            return getElementAnimationDetails(element, eventCacheKey);          }); -        /* there is no point in performing a reflow if the animation -           timeout is empty (this would cause a flicker bug normally -           in the page. There is also no point in performing an animation -           that only has a delay and no duration */ -        var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); -        var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); -        if(maxDuration === 0) { +        var transitionDuration = timings.transitionDuration; +        var animationDuration = timings.animationDuration; +        if(transitionDuration === 0 && animationDuration === 0) {            element.removeClass(className);            return false;          } -        //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 = ''; -        timings.transitionDuration > 0 ? -          blockTransitions(element) : -          blockKeyframeAnimations(element); - -        forEach(className.split(' '), function(klass, i) { -          activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; -        }); -          element.data(NG_ANIMATE_CSS_DATA_KEY, { -          className : className, -          activeClassName : activeClassName, -          maxDuration : maxDuration, -          maxDelay : maxDelay, -          classes : className + ' ' + activeClassName, -          timings : timings, +          running : formerData.running || 0, +          itemIndex : itemIndex,            stagger : stagger, -          itemIndex : itemIndex +          timings : timings, +          closeAnimationFn : angular.noop          }); +        //temporarily disable the transition so that the enter styles +        //don't animate twice (this is here to avoid a bug in Chrome/FF). +        var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass'; +        if(transitionDuration > 0) { +          blockTransitions(element, className, isCurrentlyAnimating); +        } +        if(animationDuration > 0) { +          blockKeyframeAnimations(element); +        } +          return true;        } -      function blockTransitions(element) { -        extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; +      function isStructuralAnimation(className) { +        return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave'; +      } + +      function blockTransitions(element, className, isAnimating) { +        if(isStructuralAnimation(className) || !isAnimating) { +          extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; +        } else { +          element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME); +        }        }        function blockKeyframeAnimations(element) {          extractElementNode(element).style[ANIMATION_PROP] = 'none 0s';        } -      function unblockTransitions(element) { +      function unblockTransitions(element, className) {          var prop = TRANSITION_PROP + PROPERTY_KEY;          var node = extractElementNode(element);          if(node.style[prop] && node.style[prop].length > 0) {            node.style[prop] = '';          } +        element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME);        }        function unblockKeyframeAnimations(element) { @@ -1230,22 +1286,28 @@ angular.module('ngAnimate', ['ng'])          }        } -      function animateRun(element, className, activeAnimationComplete) { -        var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); +      function animateRun(animationEvent, element, className, activeAnimationComplete) {          var node = extractElementNode(element); +        var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);          if(node.className.indexOf(className) == -1 || !elementData) {            activeAnimationComplete();            return;          } -        var timings = elementData.timings; +        var activeClassName = ''; +        forEach(className.split(' '), function(klass, i) { +          activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; +        }); +          var stagger = elementData.stagger; -        var maxDuration = elementData.maxDuration; -        var activeClassName = elementData.activeClassName; -        var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * ONE_SECOND; +        var timings = elementData.timings; +        var itemIndex = elementData.itemIndex; +        var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); +        var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); +        var maxDelayTime = maxDelay * ONE_SECOND; +          var startTime = Date.now();          var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; -        var itemIndex = elementData.itemIndex;          var style = '', appliedStyles = [];          if(timings.transitionDuration > 0) { @@ -1287,6 +1349,13 @@ angular.module('ngAnimate', ['ng'])            onEnd();            activeAnimationComplete();          }; + +        var staggerTime       = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); +        var animationTime     = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; +        var totalTime         = (staggerTime + animationTime) * ONE_SECOND; + +        elementData.running++; +        animationCloseHandler(element, totalTime);          return onEnd;          // This will automatically be called by $animate so @@ -1333,28 +1402,28 @@ angular.module('ngAnimate', ['ng'])          return style;        } -      function animateBefore(element, className, calculationDecorator) { -        if(animateSetup(element, className, calculationDecorator)) { +      function animateBefore(animationEvent, element, className, calculationDecorator) { +        if(animateSetup(animationEvent, element, className, calculationDecorator)) {            return function(cancelled) {              cancelled && animateClose(element, className);            };          }        } -      function animateAfter(element, className, afterAnimationComplete) { +      function animateAfter(animationEvent, element, className, afterAnimationComplete) {          if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { -          return animateRun(element, className, afterAnimationComplete); +          return animateRun(animationEvent, element, className, afterAnimationComplete);          } else {            animateClose(element, className);            afterAnimationComplete();          }        } -      function animate(element, className, animationComplete) { +      function animate(animationEvent, element, className, animationComplete) {          //If the animateSetup function doesn't bother returning a          //cancellation function then it means that there is no animation          //to perform at all -        var preReflowCancellation = animateBefore(element, className); +        var preReflowCancellation = animateBefore(animationEvent, element, className);          if(!preReflowCancellation) {            animationComplete();            return; @@ -1367,12 +1436,12 @@ angular.module('ngAnimate', ['ng'])          //happen in the first place          var cancel = preReflowCancellation;          afterReflow(element, function() { -          unblockTransitions(element); +          unblockTransitions(element, className);            unblockKeyframeAnimations(element);            //once the reflow is complete then we point cancel to            //the new cancellation function which will remove all of the            //animation properties from the active animation -          cancel = animateAfter(element, className, animationComplete); +          cancel = animateAfter(animationEvent, element, className, animationComplete);          });          return function(cancelled) { @@ -1382,54 +1451,59 @@ angular.module('ngAnimate', ['ng'])        function animateClose(element, className) {          element.removeClass(className); -        element.removeData(NG_ANIMATE_CSS_DATA_KEY); +        var data = element.data(NG_ANIMATE_CSS_DATA_KEY); +        if(data) { +          if(data.running) { +            data.running--; +          } +          if(!data.running || data.running === 0) { +            element.removeData(NG_ANIMATE_CSS_DATA_KEY); +          } +        }        }        return { -        allowCancel : function(element, animationEvent, className) { -          //always cancel the current animation if it is a -          //structural animation -          var oldClasses = (element.data(NG_ANIMATE_CSS_DATA_KEY) || {}).classes; -          if(!oldClasses || ['enter','leave','move'].indexOf(animationEvent) >= 0) { -            return true; -          } - -          var parentElement = element.parent(); -          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'); -          clone.removeAttr('id'); -          clone.empty(); - -          forEach(oldClasses.split(' '), function(klass) { -            clone.removeClass(klass); -          }); - -          var suffix = animationEvent == 'addClass' ? '-add' : '-remove'; -          clone.addClass(suffixClasses(className, suffix)); -          parentElement.append(clone); - -          var timings = getElementAnimationDetails(clone); -          clone.remove(); - -          return Math.max(timings.transitionDuration, timings.animationDuration) > 0; -        }, -          enter : function(element, animationCompleted) { -          return animate(element, 'ng-enter', animationCompleted); +          return animate('enter', element, 'ng-enter', animationCompleted);          },          leave : function(element, animationCompleted) { -          return animate(element, 'ng-leave', animationCompleted); +          return animate('leave', element, 'ng-leave', animationCompleted);          },          move : function(element, animationCompleted) { -          return animate(element, 'ng-move', animationCompleted); +          return animate('move', element, 'ng-move', animationCompleted); +        }, + +        beforeSetClass : function(element, add, remove, animationCompleted) { +          var className = suffixClasses(remove, '-remove') + ' ' + +                          suffixClasses(add, '-add'); +          var cancellationMethod = animateBefore('setClass', element, className, function(fn) { +            /* when classes are removed from an element then the transition style +             * that is applied is the transition defined on the element without the +             * CSS class being there. This is how CSS3 functions outside of ngAnimate. +             * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ +            var klass = element.attr('class'); +            element.removeClass(remove); +            element.addClass(add); +            var timings = fn(); +            element.attr('class', klass); +            return timings; +          }); + +          if(cancellationMethod) { +            afterReflow(element, function() { +              unblockTransitions(element, className); +              unblockKeyframeAnimations(element); +              animationCompleted(); +            }); +            return cancellationMethod; +          } +          animationCompleted();          },          beforeAddClass : function(element, className, animationCompleted) { -          var cancellationMethod = animateBefore(element, suffixClasses(className, '-add'), function(fn) { +          var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) {              /* when a CSS class is added to an element then the transition style that               * is applied is the transition defined on the element when the CSS class @@ -1443,7 +1517,7 @@ angular.module('ngAnimate', ['ng'])            if(cancellationMethod) {              afterReflow(element, function() { -              unblockTransitions(element); +              unblockTransitions(element, className);                unblockKeyframeAnimations(element);                animationCompleted();              }); @@ -1452,12 +1526,19 @@ angular.module('ngAnimate', ['ng'])            animationCompleted();          }, +        setClass : function(element, add, remove, animationCompleted) { +          remove = suffixClasses(remove, '-remove'); +          add = suffixClasses(add, '-add'); +          var className = remove + ' ' + add; +          return animateAfter('setClass', element, className, animationCompleted); +        }, +          addClass : function(element, className, animationCompleted) { -          return animateAfter(element, suffixClasses(className, '-add'), animationCompleted); +          return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted);          },          beforeRemoveClass : function(element, className, animationCompleted) { -          var cancellationMethod = animateBefore(element, suffixClasses(className, '-remove'), function(fn) { +          var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) {              /* when classes are removed from an element then the transition style               * that is applied is the transition defined on the element without the               * CSS class being there. This is how CSS3 functions outside of ngAnimate. @@ -1471,7 +1552,7 @@ angular.module('ngAnimate', ['ng'])            if(cancellationMethod) {              afterReflow(element, function() { -              unblockTransitions(element); +              unblockTransitions(element, className);                unblockKeyframeAnimations(element);                animationCompleted();              }); @@ -1481,7 +1562,7 @@ angular.module('ngAnimate', ['ng'])          },          removeClass : function(element, className, animationCompleted) { -          return animateAfter(element, suffixClasses(className, '-remove'), animationCompleted); +          return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted);          }        }; diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 6b8868f7..ba79fc88 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -782,7 +782,8 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])          }        }; -      angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) { +      angular.forEach( +        ['enter','leave','move','addClass','removeClass','setClass'], function(method) {          animate[method] = function() {            animate.queue.push({              event : method, diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index e9ab15e4..98b1650f 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -4666,11 +4666,9 @@ describe('$compile', function() {          $rootScope.$digest();          data = $animate.queue.shift(); -        expect(data.event).toBe('removeClass'); -        expect(data.args[1]).toBe('rice'); -        data = $animate.queue.shift(); -        expect(data.event).toBe('addClass'); +        expect(data.event).toBe('setClass');          expect(data.args[1]).toBe('dice'); +        expect(data.args[2]).toBe('rice');          expect(element.hasClass('ice')).toBe(true);          expect(element.hasClass('dice')).toBe(true); diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index b162fea6..b11c4766 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -335,8 +335,7 @@ describe('ngClass animations', function() {        $rootScope.val = 'two';        $rootScope.$digest(); -      expect($animate.queue.shift().event).toBe('removeClass'); -      expect($animate.queue.shift().event).toBe('addClass'); +      expect($animate.queue.shift().event).toBe('setClass');        expect($animate.queue.length).toBe(0);      });    }); @@ -450,12 +449,9 @@ describe('ngClass animations', function() {        $rootScope.$digest();        item = $animate.queue.shift(); -      expect(item.event).toBe('removeClass'); -      expect(item.args[1]).toBe('two'); - -      item = $animate.queue.shift(); -      expect(item.event).toBe('addClass'); +      expect(item.event).toBe('setClass');        expect(item.args[1]).toBe('three'); +      expect(item.args[2]).toBe('two');        expect($animate.queue.length).toBe(0);      }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 8da3d1cb..d11cfa9e 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -491,7 +491,7 @@ describe("ngAnimate", function() {              $animate.triggerReflow();              //this is to verify that the existing style is appended with a semicolon automatically  -            expect(child.attr('style')).toMatch(/width: 20px;.+?/i); +            expect(child.attr('style')).toMatch(/width: 20px;.*?/i);              browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });            } @@ -564,7 +564,7 @@ describe("ngAnimate", function() {            });          }); -        it("should fire the cancel/end function with the correct flag in the parameters", +        it("should not apply a cancellation when addClass is done multiple times",            inject(function($animate, $rootScope, $sniffer, $timeout) {            element.append(child); @@ -572,7 +572,7 @@ describe("ngAnimate", function() {            $animate.addClass(child, 'custom-delay');            $animate.addClass(child, 'custom-long-delay'); -          expect(child.hasClass('animation-cancelled')).toBe(true); +          expect(child.hasClass('animation-cancelled')).toBe(false);            expect(child.hasClass('animation-ended')).toBe(false);            $timeout.flush(); @@ -764,7 +764,6 @@ describe("ngAnimate", function() {                $animate.addClass(element, 'ng-hide');                expect(element.hasClass('ng-hide-remove')).toBe(false); //added right away -                if($sniffer.animations) { //cleanup some pending animations                  $animate.triggerReflow();                  expect(element.hasClass('ng-hide-add')).toBe(true); @@ -1472,6 +1471,8 @@ describe("ngAnimate", function() {            expect(flag).toBe(true);            expect(element.parent().id).toBe(parent2.id); + +          dealoc(element);          })); @@ -1620,11 +1621,12 @@ describe("ngAnimate", function() {            var element = parent.find('span');            var flag = false; -          $animate.removeClass(element, 'ng-hide', function() { +          $animate.addClass(element, 'ng-hide', function() {              flag = true;            });            if($sniffer.transitions) { +            $animate.triggerReflow();              browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });            }            $timeout.flush(); @@ -2734,42 +2736,6 @@ describe("ngAnimate", function() {      }); -    it("should cancel an ongoing class-based animation only if the new class contains transition/animation CSS code", -      inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - -      if (!$sniffer.transitions) return; - -      ss.addRule('.green-add', '-webkit-transition:1s linear all;' + -                                       'transition:1s linear all;'); - -      ss.addRule('.blue-add', 'background:blue;'); - -      ss.addRule('.red-add', '-webkit-transition:1s linear all;' + -                                     'transition:1s linear all;'); - -      ss.addRule('.yellow-add', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' + -                                        'animation: some_animation 4s linear 1s 2 alternate;'); - -      var element = $compile('<div></div>')($rootScope); -      $rootElement.append(element); -      jqLite($document[0].body).append($rootElement); - -      $animate.addClass(element, 'green'); -      expect(element.hasClass('green-add')).toBe(true); -    -      $animate.addClass(element, 'blue'); -      expect(element.hasClass('blue')).toBe(true);  -      expect(element.hasClass('green-add')).toBe(true); //not cancelled - -      $animate.addClass(element, 'red'); -      expect(element.hasClass('green-add')).toBe(false); -      expect(element.hasClass('red-add')).toBe(true); - -      $animate.addClass(element, 'yellow'); -      expect(element.hasClass('red-add')).toBe(false);  -      expect(element.hasClass('yellow-add')).toBe(true); -    })); -      it("should cancel and perform the dom operation only after the reflow has run",        inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { @@ -2837,7 +2803,7 @@ describe("ngAnimate", function() {          $animate.removeClass(element, 'on');          $animate.addClass(element, 'on'); -        expect(currentAnimation).toBe(null); +        expect(currentAnimation).toBe('addClass');        });      }); @@ -3259,7 +3225,7 @@ describe("ngAnimate", function() {        expect(ready).toBe(true);      })); -    it('should avoid skip animations if the same CSS class is added / removed synchronously before the reflow kicks in', +    it('should immediately close the former animation if the same CSS class is added/removed',        inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {        if (!$sniffer.transitions) return; @@ -3281,28 +3247,15 @@ describe("ngAnimate", function() {          signature += 'B';        }); -      $timeout.flush(1); -      expect(signature).toBe('AB'); - -      signature = ''; -      $animate.removeClass(element, 'on', function() { -        signature += 'A'; -      }); -      $animate.addClass(element, 'on', function() { -        signature += 'B'; -      }); -      $animate.removeClass(element, 'on', function() { -        signature += 'C'; -      }); +      $animate.triggerReflow();        $timeout.flush(1); -      expect(signature).toBe('AB'); +      expect(signature).toBe('A'); -      $animate.triggerReflow();        browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });        $timeout.flush(1); -      expect(signature).toBe('ABC'); +      expect(signature).toBe('AB');      }));    });  }); diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index 9bcd50b3..bf134f82 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -767,8 +767,7 @@ describe('ngView animations', function() {          $rootScope.klass = 'boring';          $rootScope.$digest(); -        expect($animate.queue.shift().event).toBe('removeClass'); -        expect($animate.queue.shift().event).toBe('addClass'); +        expect($animate.queue.shift().event).toBe('setClass');          expect(item.hasClass('classy')).toBe(false);          expect(item.hasClass('boring')).toBe(true); | 
