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

Animation fixes #4573

Merged
merged 2 commits into from
Oct 23, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 105 additions & 30 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ angular.module('ngAnimate', ['ng'])
var ELEMENT_NODE = 1;
var NG_ANIMATE_STATE = '$$ngAnimateState';
var NG_ANIMATE_CLASS_NAME = 'ng-animate';
var rootAnimateState = {running:true};
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope',
function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope) {
var rootAnimateState = {disabled:true};
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$timeout', '$rootScope', '$document',
function($delegate, $injector, $sniffer, $rootElement, $timeout, $rootScope, $document) {

$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);

Expand Down Expand Up @@ -466,18 +466,17 @@ angular.module('ngAnimate', ['ng'])
}
else {
var data = element.data(NG_ANIMATE_STATE) || {};
data.structural = true;
data.running = true;
data.disabled = true;
element.data(NG_ANIMATE_STATE, data);
}
break;

case 1:
rootAnimateState.running = !value;
rootAnimateState.disabled = !value;
break;

default:
value = !rootAnimateState.running;
value = !rootAnimateState.disabled;
break;
}
return !!value;
Expand All @@ -493,35 +492,46 @@ angular.module('ngAnimate', ['ng'])
*/
function performAnimation(event, className, element, parent, after, onComplete) {
var classes = (element.attr('class') || '') + ' ' + className;
var animationLookup = (' ' + classes).replace(/\s+/g,'.'),
animations = [];
forEach(lookup(animationLookup), function(animation, index) {
animations.push({
start : animation[event]
});
});

var animationLookup = (' ' + classes).replace(/\s+/g,'.');
if (!parent) {
parent = after ? after.parent() : element.parent();
}
var disabledAnimation = { running : true };

//skip the animation if animations are disabled, a parent is already being animated
//or the element is not currently attached to the document body.
if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running || animations.length === 0) {
var matches = lookup(animationLookup);
var isClassBased = event == 'addClass' || event == 'removeClass';
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};

//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 done()) in case a NO animation is not found.
if (animationsDisabled(element, parent) || matches.length === 0) {
done();
return;
}

var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
var animations = [];
//only add animations if the currently running animation is not structural
//or if there is no animation running at all
if(!ngAnimateState.running || !(isClassBased && ngAnimateState.structural)) {
forEach(matches, function(animation) {
//add the animation to the queue to if it is allowed to be cancelled
if(!animation.allowCancel || animation.allowCancel(element, event, className)) {
animations.push({
start : animation[event]
});
}
});
}

var isClassBased = event == 'addClass' || event == 'removeClass';
if(ngAnimateState.running) {
if(isClassBased && ngAnimateState.structural) {
onComplete && onComplete();
return;
}
//this would mean that an animation was not allowed so let the existing
//animation do it's thing and close this one early
if(animations.length === 0) {
onComplete && onComplete();
return;
}

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
$timeout.cancel(ngAnimateState.flagTimer);
Expand Down Expand Up @@ -610,8 +620,39 @@ angular.module('ngAnimate', ['ng'])
}

function cleanup(element) {
element.removeClass(NG_ANIMATE_CLASS_NAME);
element.removeData(NG_ANIMATE_STATE);
if(element[0] == $rootElement[0]) {
if(!rootAnimateState.disabled) {
rootAnimateState.running = false;
rootAnimateState.structural = false;
}
}
else {
element.removeClass(NG_ANIMATE_CLASS_NAME);
element.removeData(NG_ANIMATE_STATE);
}
}

function animationsDisabled(element, parent) {
if(element == $rootElement) {
return rootAnimateState.disabled || rootAnimateState.running;
}

var validState;
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parent.length === 0 || parent[0] == $document[0]) return true;

var state = parent.data(NG_ANIMATE_STATE);
if(state && (state.disabled != null || state.running != null)) {
validState = state;
break;
}
}
while(parent = parent.parent());

return validState ? (validState.disabled || validState.running) : true;
}
}]);

Expand Down Expand Up @@ -651,6 +692,7 @@ angular.module('ngAnimate', ['ng'])
animationIterationCountKey = 'IterationCount';

var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey';
var NG_ANIMATE_CLASS_KEY = '$$ngAnimateClasses';
var lookupCache = {};
var parentCounter = 0;

Expand All @@ -669,7 +711,7 @@ angular.module('ngAnimate', ['ng'])
}

function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) {
var data = lookupCache[cacheKey];
var data = cacheKey ? lookupCache[cacheKey] : null;
if(!data) {
var transitionDuration = 0, transitionDelay = 0,
animationDuration = 0, animationDelay = 0;
Expand Down Expand Up @@ -702,7 +744,9 @@ angular.module('ngAnimate', ['ng'])
transitionDuration : transitionDuration,
animationDuration : animationDuration
};
lookupCache[cacheKey] = data;
if(cacheKey) {
lookupCache[cacheKey] = data;
}
}
return data;
}
Expand Down Expand Up @@ -769,6 +813,7 @@ angular.module('ngAnimate', ['ng'])
element.addClass(activeClassName);
});

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

// This will automatically be called by $animate so
Expand All @@ -778,6 +823,7 @@ angular.module('ngAnimate', ['ng'])
element.off(css3AnimationEvents, onAnimationProgress);
element.removeClass(className);
element.removeClass(activeClassName);
element.removeData(NG_ANIMATE_CLASS_KEY);

// Only when the animation is cancelled is the done()
// function not called for this animation therefore
Expand Down Expand Up @@ -811,6 +857,35 @@ angular.module('ngAnimate', ['ng'])
}

return {
allowCancel : function(element, event, className) {
//always cancel the current animation if it is a
//structural animation
var oldClasses = element.data(NG_ANIMATE_CLASS_KEY);
if(!oldClasses || ['enter','leave','move'].indexOf(event) >= 0) {
return true;
}

var parent = element.parent();
var clone = angular.element(element[0].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.html('');

angular.forEach(oldClasses.split(' '), function(klass) {
clone.removeClass(klass);
});

var suffix = event == 'addClass' ? '-add' : '-remove';
clone.addClass(suffixClasses(className, suffix));
parent.append(clone);

var timings = getElementAnimationDetails(clone);
clone.remove();

return Math.max(timings.transitionDuration, timings.animationDuration) > 0;
},
enter : function(element, done) {
return animate(element, 'ng-enter', done);
},
Expand Down
128 changes: 126 additions & 2 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,70 @@ describe("ngAnimate", function() {
expect($animate.enabled(1)).toBe(true);
expect($animate.enabled()).toBe(true);
});

it('should place a hard disable on all child animations', function() {
var count = 0;
module(function($animateProvider) {
$animateProvider.register('.animated', function() {
return {
addClass : function(element, className, done) {
count++;
done();
}
}
});
});
inject(function($compile, $rootScope, $animate, $sniffer, $rootElement, $timeout) {
$animate.enabled(true);

var elm1 = $compile('<div class="animated"></div>')($rootScope);
var elm2 = $compile('<div class="animated"></div>')($rootScope);
$rootElement.append(elm1);
angular.element(document.body).append($rootElement);

$animate.addClass(elm1, 'klass');
expect(count).toBe(1);

$animate.enabled(false);

$animate.addClass(elm1, 'klass2');
expect(count).toBe(1);

$animate.enabled(true);

elm1.append(elm2);

$animate.addClass(elm2, 'klass');
expect(count).toBe(2);

$animate.enabled(false, elm1);

$animate.addClass(elm2, 'klass2');
expect(count).toBe(2);
});
});

it('should skip animations if the element is attached to the $rootElement', function() {
var count = 0;
module(function($animateProvider) {
$animateProvider.register('.animated', function() {
return {
addClass : function(element, className, done) {
count++;
done();
}
}
});
});
inject(function($compile, $rootScope, $animate, $sniffer, $rootElement, $timeout) {
$animate.enabled(true);

var elm1 = $compile('<div class="animated"></div>')($rootScope);

$animate.addClass(elm1, 'klass2');
expect(count).toBe(0);
});
});
});

describe("with polyfill", function() {
Expand Down Expand Up @@ -746,8 +810,8 @@ describe("ngAnimate", function() {
expect(element.hasClass('ng-enter')).toBe(true);
expect(element.hasClass('ng-enter-active')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
$timeout.flush();
}
$timeout.flush();
expect(element.hasClass('abc')).toBe(true);

$rootScope.klass = 'xyz';
Expand All @@ -760,8 +824,8 @@ describe("ngAnimate", function() {
expect(element.hasClass('ng-enter')).toBe(true);
expect(element.hasClass('ng-enter-active')).toBe(true);
browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11000 });
$timeout.flush();
}
$timeout.flush();
expect(element.hasClass('xyz')).toBe(true);
}));

Expand Down Expand Up @@ -1920,4 +1984,64 @@ describe("ngAnimate", function() {
expect(count).toBe(40);
});
});

it("should cancel an ongoing class-based animation only if the new class contains transition/animation CSS code",
inject(function($compile, $rootScope, $animate, $sniffer) {

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 enable and disable animations properly on the root element', function() {
var count = 0;
module(function($animateProvider) {
$animateProvider.register('.animated', function() {
return {
addClass : function(element, className, done) {
count++;
done();
}
}
});
});
inject(function($compile, $rootScope, $animate, $sniffer, $rootElement, $timeout) {

$rootElement.addClass('animated');
$animate.addClass($rootElement, 'green');
expect(count).toBe(1);

$animate.addClass($rootElement, 'red');
expect(count).toBe(2);
});
});

});