Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit f5289fe

Browse files
committed
fix($animate): only cancel class-based animations if the follow-up class contains CSS transition/keyframe animation code
Closes #4463 Closes #3784
1 parent 7491280 commit f5289fe

File tree

2 files changed

+104
-22
lines changed

2 files changed

+104
-22
lines changed

src/ngAnimate/animate.js

+66-20
Original file line numberDiff line numberDiff line change
@@ -493,35 +493,47 @@ angular.module('ngAnimate', ['ng'])
493493
*/
494494
function performAnimation(event, className, element, parent, after, onComplete) {
495495
var classes = (element.attr('class') || '') + ' ' + className;
496-
var animationLookup = (' ' + classes).replace(/\s+/g,'.'),
497-
animations = [];
498-
forEach(lookup(animationLookup), function(animation, index) {
499-
animations.push({
500-
start : animation[event]
501-
});
502-
});
503-
496+
var animationLookup = (' ' + classes).replace(/\s+/g,'.');
504497
if (!parent) {
505498
parent = after ? after.parent() : element.parent();
506499
}
500+
507501
var disabledAnimation = { running : true };
502+
var matches = lookup(animationLookup);
503+
var isClassBased = event == 'addClass' || event == 'removeClass';
504+
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
508505

509-
//skip the animation if animations are disabled, a parent is already being animated
510-
//or the element is not currently attached to the document body.
511-
if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || animations.length === 0) {
506+
//skip the animation if animations are disabled, a parent is already being animated,
507+
//the element is not currently attached to the document body or then completely close
508+
//the animation if any matching animations are not found at all.
509+
//NOTE: IE8 + IE9 should close properly (run done()) in case a NO animation is not found.
510+
if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || matches.length == 0) {
512511
done();
513512
return;
514513
}
515514

516-
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
515+
var animations = [];
516+
//only add animations if the currently running animation is not structural
517+
//or if there is no animation running at all
518+
if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) {
519+
forEach(matches, function(animation) {
520+
//add the animation to the queue to if it is allowed to be cancelled
521+
if(!animation.allowCancel || animation.allowCancel(element, event, className)) {
522+
animations.push({
523+
start : animation[event]
524+
});
525+
}
526+
});
527+
}
517528

518-
var isClassBased = event == 'addClass' || event == 'removeClass';
519-
if(ngAnimateState.running) {
520-
if(isClassBased && ngAnimateState.structural) {
521-
onComplete && onComplete();
522-
return;
523-
}
529+
//this would mean that an animation was not allowed so let the existing
530+
//animation do it's thing and close this one early
531+
if(animations.length == 0) {
532+
onComplete && onComplete();
533+
return;
534+
}
524535

536+
if(ngAnimateState.running) {
525537
//if an animation is currently running on the element then lets take the steps
526538
//to cancel that animation and fire any required callbacks
527539
$timeout.cancel(ngAnimateState.flagTimer);
@@ -651,6 +663,7 @@ angular.module('ngAnimate', ['ng'])
651663
animationIterationCountKey = 'IterationCount';
652664

653665
var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey';
666+
var NG_ANIMATE_CLASS_KEY = '$$ngAnimateClasses';
654667
var lookupCache = {};
655668
var parentCounter = 0;
656669

@@ -669,7 +682,7 @@ angular.module('ngAnimate', ['ng'])
669682
}
670683

671684
function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) {
672-
var data = lookupCache[cacheKey];
685+
var data = cacheKey ? lookupCache[cacheKey] : null;
673686
if(!data) {
674687
var transitionDuration = 0, transitionDelay = 0,
675688
animationDuration = 0, animationDelay = 0;
@@ -702,7 +715,9 @@ angular.module('ngAnimate', ['ng'])
702715
transitionDuration : transitionDuration,
703716
animationDuration : animationDuration
704717
};
705-
lookupCache[cacheKey] = data;
718+
if(cacheKey) {
719+
lookupCache[cacheKey] = data;
720+
}
706721
}
707722
return data;
708723
}
@@ -769,6 +784,7 @@ angular.module('ngAnimate', ['ng'])
769784
element.addClass(activeClassName);
770785
});
771786

787+
element.data(NG_ANIMATE_CLASS_KEY, className + ' ' + activeClassName);
772788
element.on(css3AnimationEvents, onAnimationProgress);
773789

774790
// This will automatically be called by $animate so
@@ -778,6 +794,7 @@ angular.module('ngAnimate', ['ng'])
778794
element.off(css3AnimationEvents, onAnimationProgress);
779795
element.removeClass(className);
780796
element.removeClass(activeClassName);
797+
element.removeData(NG_ANIMATE_CLASS_KEY);
781798

782799
// Only when the animation is cancelled is the done()
783800
// function not called for this animation therefore
@@ -811,6 +828,35 @@ angular.module('ngAnimate', ['ng'])
811828
}
812829

813830
return {
831+
allowCancel : function(element, event, className) {
832+
//always cancel the current animation if it is a
833+
//structural animation
834+
var oldClasses = element.data(NG_ANIMATE_CLASS_KEY);
835+
if(!oldClasses || ['enter','leave','move'].indexOf(event) >= 0) {
836+
return true;
837+
}
838+
839+
var parent = element.parent();
840+
var clone = angular.element(element[0].cloneNode());
841+
842+
//make the element super hidden and override any CSS style values
843+
clone.attr('style','position:absolute; top:-9999px; left:-9999px');
844+
clone.removeAttr('id');
845+
clone.html('');
846+
847+
angular.forEach(oldClasses.split(' '), function(klass) {
848+
clone.removeClass(klass);
849+
});
850+
851+
var suffix = event == 'addClass' ? '-add' : '-remove';
852+
clone.addClass(suffixClasses(className, suffix));
853+
parent.append(clone);
854+
855+
var timings = getElementAnimationDetails(clone);
856+
clone.remove();
857+
858+
return Math.max(timings.transitionDuration, timings.animationDuration) > 0;
859+
},
814860
enter : function(element, done) {
815861
return animate(element, 'ng-enter', done);
816862
},

test/ngAnimate/animateSpec.js

+38-2
Original file line numberDiff line numberDiff line change
@@ -746,8 +746,8 @@ describe("ngAnimate", function() {
746746
expect(element.hasClass('ng-enter')).toBe(true);
747747
expect(element.hasClass('ng-enter-active')).toBe(true);
748748
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
749+
$timeout.flush();
749750
}
750-
$timeout.flush();
751751
expect(element.hasClass('abc')).toBe(true);
752752

753753
$rootScope.klass = 'xyz';
@@ -760,8 +760,8 @@ describe("ngAnimate", function() {
760760
expect(element.hasClass('ng-enter')).toBe(true);
761761
expect(element.hasClass('ng-enter-active')).toBe(true);
762762
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 });
763+
$timeout.flush();
763764
}
764-
$timeout.flush();
765765
expect(element.hasClass('xyz')).toBe(true);
766766
}));
767767

@@ -1920,4 +1920,40 @@ describe("ngAnimate", function() {
19201920
expect(count).toBe(40);
19211921
});
19221922
});
1923+
1924+
it("should cancel an ongoing class-based animation only if the new class contains transition/animation CSS code",
1925+
inject(function($compile, $rootScope, $animate, $sniffer) {
1926+
1927+
if (!$sniffer.transitions) return;
1928+
1929+
ss.addRule('.green-add', '-webkit-transition:1s linear all;' +
1930+
'transition:1s linear all;');
1931+
1932+
ss.addRule('.blue-add', 'background:blue;');
1933+
1934+
ss.addRule('.red-add', '-webkit-transition:1s linear all;' +
1935+
'transition:1s linear all;');
1936+
1937+
ss.addRule('.yellow-add', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' +
1938+
'animation: some_animation 4s linear 1s 2 alternate;');
1939+
1940+
var element = $compile('<div></div>')($rootScope);
1941+
$rootElement.append(element);
1942+
jqLite($document[0].body).append($rootElement);
1943+
1944+
$animate.addClass(element, 'green');
1945+
expect(element.hasClass('green-add')).toBe(true);
1946+
1947+
$animate.addClass(element, 'blue');
1948+
expect(element.hasClass('blue')).toBe(true);
1949+
expect(element.hasClass('green-add')).toBe(true); //not cancelled
1950+
1951+
$animate.addClass(element, 'red');
1952+
expect(element.hasClass('green-add')).toBe(false);
1953+
expect(element.hasClass('red-add')).toBe(true);
1954+
1955+
$animate.addClass(element, 'yellow');
1956+
expect(element.hasClass('red-add')).toBe(false);
1957+
expect(element.hasClass('yellow-add')).toBe(true);
1958+
}));
19231959
});

0 commit comments

Comments
 (0)