fix(ngAnimate): $timeout integration and cancel callbacks added

This commit is contained in:
Matias Niemelä 2013-07-22 15:37:45 -04:00 committed by Misko Hevery
parent 7d69d52acf
commit 15389b0e37
5 changed files with 425 additions and 431 deletions

View file

@ -69,16 +69,16 @@ describe('Docs Annotations', function() {
beforeEach(function() {
module(function($provide, $animateProvider) {
$provide.value('$window', window = angular.mock.createMockWindow());
$animateProvider.register('.foldout', function($window) {
$animateProvider.register('.foldout', function($timeout) {
return {
enter : function(element, done) {
$window.setTimeout(done, 1000);
$timeout(done, 1000);
},
removeClass : function(element, className, done) {
$window.setTimeout(done, 500);
$timeout(done, 500);
},
addClass : function(element, className, done) {
$window.setTimeout(done, 200);
$timeout(done, 200);
}
}
});
@ -112,41 +112,46 @@ describe('Docs Annotations', function() {
expect(foldout.html()).toContain('loading');
}));
it('should download a foldout HTML page and animate the contents', inject(function($httpBackend) {
it('should download a foldout HTML page and animate the contents', inject(function($httpBackend, $timeout) {
$httpBackend.expect('GET', url).respond('hello');
element.triggerHandler('click');
$httpBackend.flush();
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
$timeout.flushNext(0);
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(1000);
var kids = body.children();
var foldout = angular.element(kids[kids.length-1]);
expect(foldout.text()).toContain('hello');
}));
it('should hide then show when clicked again', inject(function($httpBackend) {
it('should hide then show when clicked again', inject(function($httpBackend, $timeout) {
$httpBackend.expect('GET', url).respond('hello');
//enter
element.triggerHandler('click');
$httpBackend.flush();
window.setTimeout.expect(1).process();
window.setTimeout.expect(1000).process();
window.setTimeout.expect(0).process();
$timeout.flushNext(0);
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(1000);
//hide
element.triggerHandler('click');
window.setTimeout.expect(1).process();
window.setTimeout.expect(200).process();
window.setTimeout.expect(0).process();
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(200);
$timeout.flushNext(0);
//show
element.triggerHandler('click');
window.setTimeout.expect(1).process();
window.setTimeout.expect(500).process();
window.setTimeout.expect(0).process();
$timeout.flushNext(1);
$timeout.flushNext(0);
$timeout.flushNext(500);
$timeout.flushNext(0);
}));
});

View file

@ -56,7 +56,7 @@ var $AnimateProvider = ['$provide', function($provide) {
$provide.factory(name, factory);
};
this.$get = function() {
this.$get = ['$timeout', function($timeout) {
return {
enter : function(element, parent, after, done) {
var afterNode = after && after[after.length - 1];
@ -66,12 +66,12 @@ var $AnimateProvider = ['$provide', function($provide) {
forEach(element, function(node) {
parentNode.insertBefore(node, afterNextSibling);
});
(done || noop)();
$timeout(done || noop, 0, false);
},
leave : function(element, done) {
element.remove();
(done || noop)();
$timeout(done || noop, 0, false);
},
move : function(element, parent, after, done) {
@ -85,7 +85,7 @@ var $AnimateProvider = ['$provide', function($provide) {
className :
isArray(className) ? className.join(' ') : '';
element.addClass(className);
(done || noop)();
$timeout(done || noop, 0, false);
},
removeClass : function(element, className, done) {
@ -93,10 +93,10 @@ var $AnimateProvider = ['$provide', function($provide) {
className :
isArray(className) ? className.join(' ') : '';
element.removeClass(className);
(done || noop)();
$timeout(done || noop, 0, false);
},
enabled : noop
};
};
}];
}];

View file

@ -203,15 +203,15 @@ angular.module('ngAnimate', ['ng'])
var NG_ANIMATE_STATE = '$$ngAnimateState';
var rootAnimateState = {running:true};
$provide.decorator('$animate', ['$delegate', '$injector', '$window', '$sniffer', '$rootElement',
function($delegate, $injector, $window, $sniffer, $rootElement) {
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement',
function($delegate, $injector, $sniffer, $rootElement) {
var noop = angular.noop;
var forEach = angular.forEach;
$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
function lookup(name) {
function lookup(name) {
if (name) {
var classes = name.substr(1).split('.'),
classMap = {};
@ -241,7 +241,7 @@ angular.module('ngAnimate', ['ng'])
/**
* @ngdoc object
* @name ngAnimate.$animate
* @requires $window, $sniffer, $rootElement
* @requires $timeout, $sniffer, $rootElement
* @function
*
* @description
@ -444,80 +444,72 @@ angular.module('ngAnimate', ['ng'])
and the onComplete callback will be fired once the animation is fully complete.
*/
function performAnimation(event, className, element, parent, after, onComplete) {
if(nothingToAnimate(className, element)) {
var classes = ((element.attr('class') || '') + ' ' + className),
animationLookup = (' ' + classes).replace(/\s+/g,'.'),
animations = [];
forEach(lookup(animationLookup), function(animation, index) {
animations.push({
start : animation[event]
});
});
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) {
//avoid calling done() since there is no need to remove any
//data or className values since this happens earlier than that
(onComplete || noop)();
} else {
var classes = ((element.attr('class') || '') + ' ' + className),
animationLookup = (' ' + classes).replace(/\s+/g,'.'),
animations = [];
forEach(lookup(animationLookup), function(animation, index) {
animations.push({
start : animation[event],
done : false
});
});
return;
}
if (!parent) {
parent = after ? after.parent() : element.parent();
}
var disabledAnimation = { running : true };
var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
//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) {
//avoid calling done() since there is no need to remove any
//data or className values since this happens earlier than that
(onComplete || noop)();
return;
}
//if an animation is currently running on the element then lets take the steps
//to cancel that animation and fire any required callbacks
if(ngAnimateState.running) {
cancelAnimations(ngAnimateState.animations);
ngAnimateState.done();
}
var animationData = element.data(NG_ANIMATE_STATE) || {};
element.data(NG_ANIMATE_STATE, {
running:true,
animations:animations,
done:done
});
//if an animation is currently running on the element then lets take the steps
//to cancel that animation and fire any required callbacks
if(animationData.running) {
cancelAnimations(animationData.animations);
animationData.done();
}
if(event == 'addClass') {
className = suffixClasses(className, '-add');
} else if(event == 'removeClass') {
className = suffixClasses(className, '-remove');
}
element.data(NG_ANIMATE_STATE, {
running:true,
animations:animations,
done:done
});
element.addClass(className);
if(event == 'addClass') {
className = suffixClasses(className, '-add');
} else if(event == 'removeClass') {
className = suffixClasses(className, '-remove');
}
forEach(animations, function(animation, index) {
var fn = function() {
progress(index);
};
element.addClass(className);
forEach(animations, function(animation, index) {
var fn = function() {
progress(index);
};
if(animation.start) {
if(event == 'addClass' || event == 'removeClass') {
animation.cancel = animation.start(element, className, fn);
} else {
animation.cancel = animation.start(element, fn);
}
if(animation.start) {
if(event == 'addClass' || event == 'removeClass') {
animation.endFn = animation.start(element, className, fn);
} else {
fn();
animation.endFn = animation.start(element, fn);
}
});
}
function nothingToAnimate(className, element) {
return !(className && className.length > 0 && element.length > 0);
}
} else {
fn();
}
});
function cancelAnimations(animations) {
var isCancelledFlag = true;
forEach(animations, function(animation) {
(animation.cancel || noop)(element);
(animation.endFn || noop)(isCancelledFlag);
});
}
@ -534,6 +526,7 @@ angular.module('ngAnimate', ['ng'])
function progress(index) {
animations[index].done = true;
(animations[index].endFn || noop)();
for(var i=0;i<animations.length;i++) {
if(!animations[i].done) return;
}
@ -552,7 +545,7 @@ angular.module('ngAnimate', ['ng'])
}]);
}])
.animation('', ['$window','$sniffer', function($window, $sniffer) {
.animation('', ['$window','$sniffer', '$timeout', function($window, $sniffer, $timeout) {
return {
enter : function(element, done) {
return animate(element, 'ng-enter', done);
@ -576,13 +569,13 @@ angular.module('ngAnimate', ['ng'])
done();
} else {
var activeClassName = '';
$window.setTimeout(startAnimation, 1);
$timeout(startAnimation, 1, false);
//this acts as the cancellation function in case
//a new animation is triggered while another animation
//is still going on (otherwise the active className
//would still hang around until the timer is complete).
return onComplete;
return onEnd;
}
function parseMaxTime(str) {
@ -643,12 +636,21 @@ angular.module('ngAnimate', ['ng'])
}
});
$window.setTimeout(onComplete, duration * 1000);
$timeout(done, duration * 1000, false);
}
function onComplete() {
//this will automatically be called by $animate so
//there is no need to attach this internally to the
//timeout done method
function onEnd(cancelled) {
element.removeClass(activeClassName);
done();
//only when the animation is cancelled is the done()
//function not called for this animation therefore
//this must be also called
if(cancelled) {
done();
}
};
};
}]);

File diff suppressed because it is too large Load diff

View file

@ -623,7 +623,7 @@ describe('ngView animations', function() {
});
});
inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) {
inject(function($rootScope, $compile, $location, $route, $timeout, $rootElement, $sniffer, $animate) {
element = $compile(html('<div><ng:view onload="load()" class="my-animation"></ng:view></div>'))($rootScope);
$animate.enabled(true);
@ -632,20 +632,12 @@ describe('ngView animations', function() {
$animate.process('enter'); //ngView
if($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
}
$timeout.flush();
$animate.process('enter'); //repeat 1
$animate.process('enter'); //repeat 2
if($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
}
$timeout.flush();
expect(element.text()).toEqual('12');
@ -653,29 +645,17 @@ describe('ngView animations', function() {
$rootScope.$digest();
$animate.process('leave'); //ngView old
if($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
}
$timeout.flush();
$animate.process('enter'); //ngView new
if($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
}
$timeout.flush();
expect(n(element.text())).toEqual(''); //this is midway during the animation
$animate.process('enter'); //ngRepeat 3
$animate.process('enter'); //ngRepeat 4
if($sniffer.transitions) {
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(1).process();
$window.setTimeout.expect(0).process();
$window.setTimeout.expect(0).process();
}
$timeout.flush();
expect(element.text()).toEqual('34');