diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js index e024c831cf3c..fbfc0cc78426 100644 --- a/src/ngAnimate/animateQueue.js +++ b/src/ngAnimate/animateQueue.js @@ -5,6 +5,7 @@ var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; + var ONE_SPACE = ' '; var rules = this.rules = { skip: [], @@ -12,6 +13,29 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { join: [] }; + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { return rules[ruleType].some(function(fn) { return fn(element, currentAnimation, previousAnimation); @@ -59,11 +83,19 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); rules.cancel.push(function(element, newAnimation, currentAnimation) { - var nO = newAnimation.options; - var cO = currentAnimation.options; - // if the exact same CSS class is added/removed then it's safe to cancel it - return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass); + + var nA = newAnimation.options.addClass; + var nR = newAnimation.options.removeClass; + var cA = currentAnimation.options.addClass; + var cR = currentAnimation.options.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } + + return (hasMatchingClasses(nA, cR)) || (hasMatchingClasses(nR, cA)); }); this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', diff --git a/test/ngAnimate/integrationSpec.js b/test/ngAnimate/integrationSpec.js index 4a0610f2170a..5c67897c8492 100644 --- a/test/ngAnimate/integrationSpec.js +++ b/test/ngAnimate/integrationSpec.js @@ -387,6 +387,45 @@ describe('ngAnimate integration tests', function() { dealoc(element); })); + + + it("should remove a class when the same class is currently being added by a joined class-based animation", + inject(function($animate, $animateCss, $rootScope, $document, $rootElement, $$rAF) { + + ss.addRule('.hide', 'opacity: 0'); + ss.addRule('.hide-add, .hide-remove', 'transition: 1s linear all'); + + jqLite($document[0].body).append($rootElement); + element = jqLite('
'); + $rootElement.append(element); + + // These animations will be joined together + $animate.addClass(element, 'red'); + $animate.addClass(element, 'hide'); + $rootScope.$digest(); + + expect(element).toHaveClass('red-add'); + expect(element).toHaveClass('hide-add'); + + // When a digest has passed, but no $rAF has been issued yet, .hide hasn't been added to + // the element yet + $animate.removeClass(element, 'hide'); + $rootScope.$digest(); + $$rAF.flush(); + + expect(element).not.toHaveClass('hide-add hide-add-active'); + expect(element).toHaveClass('hide-remove hide-remove-active'); + + //End the animation process + browserTrigger(element, 'transitionend', + { timeStamp: Date.now() + 1000, elapsedTime: 2 }); + $animate.flush(); + + expect(element).not.toHaveClass('hide-add-active red-add-active'); + expect(element).toHaveClass('red'); + expect(element).not.toHaveClass('hide'); + })); + }); describe('JS animations', function() {