diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 5de773c6fbc7..a8cb6263a0cf 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -469,23 +469,8 @@ angular.module('ngAnimate', ['ng']) function resolveElementClasses(element, cache, runningAnimations) { runningAnimations = runningAnimations || {}; - var map = {}; - forEach(cache.add, function(className) { - if (className && className.length) { - map[className] = map[className] || 0; - map[className]++; - } - }); - - forEach(cache.remove, function(className) { - if (className && className.length) { - map[className] = map[className] || 0; - map[className]--; - } - }); - - var lookup = []; + var lookup = {}; forEach(runningAnimations, function(data, selector) { forEach(selector.split(' '), function(s) { lookup[s]=data; @@ -493,7 +478,7 @@ angular.module('ngAnimate', ['ng']) }); var toAdd = [], toRemove = []; - forEach(map, function(status, className) { + forEach(cache.classes, function(status, className) { var hasClass = angular.$$hasClass(element[0], className); var matchingAnimation = lookup[className] || {}; @@ -505,12 +490,12 @@ angular.module('ngAnimate', ['ng']) // Once an animation is allowed then the code will also check to see if // there exists any on-going animation that is already adding or remvoing // the matching CSS class. - if (status < 0) { + if (status === false) { //does it have the class or will it have the class if (hasClass || matchingAnimation.event == 'addClass') { toRemove.push(className); } - } else if (status > 0) { + } else if (status === true) { //is the class missing or will it be removed? if (!hasClass || matchingAnimation.event == 'removeClass') { toAdd.push(className); @@ -997,20 +982,36 @@ angular.module('ngAnimate', ['ng']) return $delegate.setClass(element, add, remove); } + // we're using a combined array for both the add and remove + // operations since the ORDER OF addClass and removeClass matters + var classes, cache = element.data(STORAGE_KEY); + var hasCache = !!cache; + if (!cache) { + cache = {}; + cache.classes = {}; + } + classes = cache.classes; + add = isArray(add) ? add : add.split(' '); - remove = isArray(remove) ? remove : remove.split(' '); + forEach(add, function(c) { + if (c && c.length) { + classes[c] = true; + } + }); - var cache = element.data(STORAGE_KEY); - if (cache) { - cache.add = cache.add.concat(add); - cache.remove = cache.remove.concat(remove); + remove = isArray(remove) ? remove : remove.split(' '); + forEach(remove, function(c) { + if (c && c.length) { + classes[c] = false; + } + }); + if (hasCache) { //the digest cycle will combine all the animations into one function return cache.promise; } else { element.data(STORAGE_KEY, cache = { - add : add, - remove : remove + classes : classes }); } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 0bd9c891c1e1..e2440b343c1a 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -3408,11 +3408,76 @@ describe("ngAnimate", function() { $animate.triggerReflow(); expect(log.length).toBe(2); - expect(log[0]).toEqual({ name : 'addClass', className : 'one five' }); + expect(log[0]).toEqual({ name : 'addClass', className : 'one four five' }); expect(log[1]).toEqual({ name : 'removeClass', className : 'three' }); }); }); + it('should intelligently cancel out redundant class-based animations', function() { + var log = []; + var track = function(name) { + return function() { + log.push({ name : name, className : arguments[1] }); + }; + }; + module(function($animateProvider) { + $animateProvider.register('.animate', function() { + return { + addClass : track('addClass'), + removeClass : track('removeClass') + }; + }); + }); + inject(function($rootScope, $animate, $compile, $rootElement, $document) { + $animate.enabled(true); + + var element = $compile('
')($rootScope); + $rootElement.append(element); + angular.element($document[0].body).append($rootElement); + + $animate.removeClass(element, 'one'); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(log.length).toBe(0); + $animate.triggerCallbacks(); + + $animate.addClass(element, 'two'); + $animate.addClass(element, 'two'); + $animate.removeClass(element, 'two'); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(log.length).toBe(0); + $animate.triggerCallbacks(); + + $animate.removeClass(element, 'three'); + $animate.addClass(element, 'three'); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(log.length).toBe(0); + $animate.triggerCallbacks(); + + $animate.removeClass(element, 'four'); + $animate.addClass(element, 'four'); + $animate.removeClass(element, 'four'); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(log.length).toBe(1); + $animate.triggerCallbacks(); + expect(log[0]).toEqual({ name : 'removeClass', className : 'four' }); + + $animate.addClass(element, 'five'); + $animate.addClass(element, 'five'); + $animate.addClass(element, 'five'); + $animate.removeClass(element, 'five'); + $animate.addClass(element, 'five'); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(log.length).toBe(2); + $animate.triggerCallbacks(); + expect(log[1]).toEqual({ name : 'addClass', className : 'five' }); + }); + }); + it('should skip class-based animations if the element is removed before the digest occurs', function() { var spy = jasmine.createSpy(); module(function($animateProvider) {