diff --git a/src/ng/animate.js b/src/ng/animate.js index 569910151314..609ebf946333 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -122,7 +122,8 @@ var $AnimateProvider = ['$provide', function($provide) { } }); - return (toAdd.length + toRemove.length) > 0 && [toAdd.length && toAdd, toRemove.length && toRemove]; + return (toAdd.length + toRemove.length) > 0 && + [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null]; } function cachedClassManipulation(cache, classes, op) { @@ -144,6 +145,13 @@ var $AnimateProvider = ['$provide', function($provide) { return currentDefer.promise; } + function applyStyles(element, options) { + if (angular.isObject(options)) { + var styles = extend(options.from || {}, options.to || {}); + element.css(styles); + } + } + /** * * @ngdoc service @@ -162,6 +170,10 @@ var $AnimateProvider = ['$provide', function($provide) { * page}. */ return { + animate : function(element, from, to) { + applyStyles(element, { from: from, to: to }); + return asyncPromise(); + }, /** * @@ -176,9 +188,11 @@ var $AnimateProvider = ['$provide', function($provide) { * a child (if the after element is not present) * @param {DOMElement} after the sibling element which will append the element * after itself + * @param {object=} options an optional collection of styles that will be applied to the element. * @return {Promise} the animation callback promise */ - enter : function(element, parent, after) { + enter : function(element, parent, after, options) { + applyStyles(element, options); after ? after.after(element) : parent.prepend(element); return asyncPromise(); @@ -192,9 +206,10 @@ var $AnimateProvider = ['$provide', function($provide) { * @description Removes the element from the DOM. When the function is called a promise * is returned that will be resolved at a later time. * @param {DOMElement} element the element which will be removed from the DOM + * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - leave : function(element) { + leave : function(element, options) { element.remove(); return asyncPromise(); }, @@ -214,12 +229,13 @@ var $AnimateProvider = ['$provide', function($provide) { * inserted into (if the after element is not present) * @param {DOMElement} after the sibling element where the element will be * positioned next to + * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - move : function(element, parent, after) { + move : function(element, parent, after, options) { // Do not remove element before insert. Removing will cause data associated with the // element to be dropped. Insert will implicitly do the remove. - return this.enter(element, parent, after); + return this.enter(element, parent, after, options); }, /** @@ -232,13 +248,14 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {DOMElement} element the element which will have the className value * added to it * @param {string} className the CSS class which will be added to the element + * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - addClass : function(element, className) { - return this.setClass(element, className, []); + addClass : function(element, className, options) { + return this.setClass(element, className, [], options); }, - $$addClassImmediately : function(element, className) { + $$addClassImmediately : function(element, className, options) { element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') @@ -246,6 +263,8 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteAddClass(element, className); }); + applyStyles(element, options); + return asyncPromise(); }, /** @@ -258,13 +277,14 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {DOMElement} element the element which will have the className value * removed from it * @param {string} className the CSS class which will be removed from the element + * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - removeClass : function(element, className) { - return this.setClass(element, [], className); + removeClass : function(element, className, options) { + return this.setClass(element, [], className, options); }, - $$removeClassImmediately : function(element, className) { + $$removeClassImmediately : function(element, className, options) { element = jqLite(element); className = !isString(className) ? (isArray(className) ? className.join(' ') : '') @@ -272,6 +292,7 @@ var $AnimateProvider = ['$provide', function($provide) { forEach(element, function (element) { jqLiteRemoveClass(element, className); }); + applyStyles(element, options); return asyncPromise(); }, @@ -286,9 +307,10 @@ var $AnimateProvider = ['$provide', function($provide) { * removed from it * @param {string} add the CSS classes which will be added to the element * @param {string} remove the CSS class which will be removed from the element + * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove) { + setClass : function(element, add, remove, options) { var self = this; var STORAGE_KEY = '$$animateClasses'; var createdCache = false; @@ -297,9 +319,12 @@ var $AnimateProvider = ['$provide', function($provide) { var cache = element.data(STORAGE_KEY); if (!cache) { cache = { - classes: {} + classes: {}, + options : options }; createdCache = true; + } else if (options && cache.options) { + cache.options = angular.extend(cache.options || {}, options); } var classes = cache.classes; @@ -320,7 +345,7 @@ var $AnimateProvider = ['$provide', function($provide) { if (cache) { var classes = resolveElementClasses(element, cache.classes); if (classes) { - self.$$setClassImmediately(element, classes[0], classes[1]); + self.$$setClassImmediately(element, classes[0], classes[1], cache.options); } } @@ -332,9 +357,10 @@ var $AnimateProvider = ['$provide', function($provide) { return cache.promise; }, - $$setClassImmediately : function(element, add, remove) { + $$setClassImmediately : function(element, add, remove, options) { add && this.$$addClassImmediately(element, add); remove && this.$$removeClassImmediately(element, remove); + applyStyles(element, options); return asyncPromise(); }, diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js index ebcc05b14111..414b986a9ae6 100644 --- a/src/ng/directive/ngShowHide.js +++ b/src/ng/directive/ngShowHide.js @@ -167,7 +167,9 @@ var ngShowDirective = ['$animate', function($animate) { // we can control when the element is actually displayed on screen without having // to have a global/greedy CSS selector that breaks when other animations are run. // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 - $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { + tempClasses : NG_HIDE_IN_PROGRESS_CLASS + }); }); } }; @@ -324,7 +326,9 @@ var ngHideDirective = ['$animate', function($animate) { scope.$watch(attr.ngHide, function ngHideWatchAction(value){ // The comment inside of the ngShowDirective explains why we add and // remove a temporary class for the show/hide animation - $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, NG_HIDE_IN_PROGRESS_CLASS); + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, { + tempClasses : NG_HIDE_IN_PROGRESS_CLASS + }); }); } }; diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index a754408992ec..cecaf6fd1bca 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -83,7 +83,7 @@ * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests * are complete. * - *

CSS-defined Animations

+ * ## CSS-defined Animations * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported * and can be used to play along with this naming structure. @@ -320,6 +320,49 @@ * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation * or transition code that is defined via a stylesheet). * + * + * ### Applying Directive-specific Styles to an Animation + * In some cases a directive or service may want to provide `$animate` with extra details that the animation will + * include into its animation. Let's say for example we wanted to render an animation that animates an element + * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click + * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function + * call to `$animate.addClass`. + * + * ```js + * canvas.on('click', function(e) { + * $animate.addClass(element, 'on', { + * to: { + * left : e.client.x + 'px', + * top : e.client.y + 'px' + * } + * }): + * }); + * ``` + * + * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will + * also include and transition the styling of the `left` and `top` properties into its running animation. If we want + * to provide some starting animation values then we can do so by placing the starting animations styles into an object + * called `from` in the same object as the `to` animations. + * + * ```js + * canvas.on('click', function(e) { + * $animate.addClass(element, 'on', { + * from: { + * position: 'absolute', + * left: '0px', + * top: '0px' + * }, + * to: { + * left : e.client.x + 'px', + * top : e.client.y + 'px' + * } + * }): + * }); + * ``` + * + * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the + * element. If `ngAnimate` is not present then the styles will be applied immediately. + * */ angular.module('ngAnimate', ['ng']) @@ -378,6 +421,7 @@ angular.module('ngAnimate', ['ng']) var selectors = $animateProvider.$$selectors; var isArray = angular.isArray; var isString = angular.isString; + var isObject = angular.isObject; var ELEMENT_NODE = 1; var NG_ANIMATE_STATE = '$$ngAnimateState'; @@ -472,8 +516,12 @@ angular.module('ngAnimate', ['ng']) // some plugin code may still be passing in the callback // function as the last param for the $animate methods so // it's best to only allow string or array values for now - if (isArray(options)) return options; - if (isString(options)) return [options]; + if (isObject(options)) { + if (options.tempClasses && isString(options.tempClasses)) { + options.tempClasses = options.tempClasses.split(/\s+/); + } + return options; + } } function resolveElementClasses(element, cache, runningAnimations) { @@ -550,7 +598,7 @@ angular.module('ngAnimate', ['ng']) } } - function animationRunner(element, animationEvent, className) { + function animationRunner(element, animationEvent, className, options) { //transcluded directives may sometimes fire an animation using only comment nodes //best to catch this early on to prevent any animation operations from occurring var node = element[0]; @@ -558,6 +606,11 @@ angular.module('ngAnimate', ['ng']) return; } + if (options) { + options.to = options.to || {}; + options.from = options.from || {}; + } + var classNameAdd; var classNameRemove; if (isArray(className)) { @@ -575,9 +628,10 @@ angular.module('ngAnimate', ['ng']) } var isSetClassOperation = animationEvent == 'setClass'; - var isClassBased = isSetClassOperation || - animationEvent == 'addClass' || - animationEvent == 'removeClass'; + var isClassBased = isSetClassOperation + || animationEvent == 'addClass' + || animationEvent == 'removeClass' + || animationEvent == 'animate'; var currentClassName = element.attr('class'); var classes = currentClassName + ' ' + className; @@ -645,16 +699,19 @@ angular.module('ngAnimate', ['ng']) }; switch(animation.event) { case 'setClass': - cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress)); + cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options)); + break; + case 'animate': + cancellations.push(animation.fn(element, className, options.from, options.to, progress)); break; case 'addClass': - cancellations.push(animation.fn(element, classNameAdd || className, progress)); + cancellations.push(animation.fn(element, classNameAdd || className, progress, options)); break; case 'removeClass': - cancellations.push(animation.fn(element, classNameRemove || className, progress)); + cancellations.push(animation.fn(element, classNameRemove || className, progress, options)); break; default: - cancellations.push(animation.fn(element, progress)); + cancellations.push(animation.fn(element, progress, options)); break; } }); @@ -670,6 +727,11 @@ angular.module('ngAnimate', ['ng']) className : className, isClassBased : isClassBased, isSetClassOperation : isSetClassOperation, + applyStyles : function() { + if (options) { + element.css(angular.extend(options.from || {}, options.to || {})); + } + }, before : function(allCompleteFn) { beforeComplete = allCompleteFn; run(before, beforeCancel, function() { @@ -761,6 +823,65 @@ angular.module('ngAnimate', ['ng']) * */ return { + /** + * @ngdoc method + * @name $animate#animate + * @kind function + * + * @description + * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element. + * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation + * will take on the provided styles. For example, if a transition animation is set for the given className then the + * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is + * detected then the provided styles will be given in as function paramters. + * + * ```js + * ngModule.animation('.my-inline-animation', function() { + * return { + * animate : function(element, className, from, to, done) { + * //styles + * } + * } + * }); + * ``` + * + * Below is a breakdown of each step that occurs during the `animate` animation: + * + * | Animation Step | What the element class attribute looks like | + * |-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| + * | 1. $animate.animate(...) is called | class="my-animation" | + * | 2. $animate waits for the next digest to start the animation | class="my-animation ng-animate" | + * | 3. $animate runs the JavaScript-defined animations detected on the element | class="my-animation ng-animate" | + * | 4. the className class value is added to the element | class="my-animation ng-animate className" | + * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate className" | + * | 6. $animate blocks all CSS transitions on the element to ensure the .className class styling is applied right away| class="my-animation ng-animate className" | + * | 7. $animate applies the provided collection of `from` CSS styles to the element | class="my-animation ng-animate className" | + * | 8. $animate waits for a single animation frame (this performs a reflow) | class="my-animation ng-animate className" | + * | 9. $animate removes the CSS transition block placed on the element | class="my-animation ng-animate className" | + * | 10. the className-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-animate className className-active" | + * | 11. $animate applies the collection of `to` CSS styles to the element which are then handled by the transition | class="my-animation ng-animate className className-active" | + * | 12. $animate waits for the animation to complete (via events and timeout) | class="my-animation ng-animate className className-active" | + * | 13. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | + * | 14. The returned promise is resolved. | class="my-animation" | + * + * @param {DOMElement} element the element that will be the focus of the enter animation + * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation + * @param {object} to a collection of CSS styles that the element will animate towards + * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`) + * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation + * @return {Promise} the animation callback promise + */ + animate : function(element, from, to, className, options) { + className = className || 'ng-inline-animate'; + options = parseAnimateOptions(options) || {}; + options.from = to ? from : null; + options.to = to ? to : from; + + return runAnimationPostDigest(function(done) { + return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done); + }); + }, + /** * @ngdoc method * @name $animate#enter @@ -791,6 +912,7 @@ angular.module('ngAnimate', ['ng']) * @param {DOMElement} element the element that will be the focus of the enter animation * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation + * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ enter : function(element, parentElement, afterElement, options) { @@ -834,6 +956,7 @@ angular.module('ngAnimate', ['ng']) * | 13. The returned promise is resolved. | ... | * * @param {DOMElement} element the element that will be the focus of the leave animation + * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ leave : function(element, options) { @@ -880,6 +1003,7 @@ angular.module('ngAnimate', ['ng']) * @param {DOMElement} element the element that will be the focus of the move animation * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation + * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ move : function(element, parentElement, afterElement, options) { @@ -923,6 +1047,7 @@ angular.module('ngAnimate', ['ng']) * * @param {DOMElement} element the element that will be animated * @param {string} className the CSS class that will be added to the element and then animated + * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ addClass : function(element, className, options) { @@ -956,6 +1081,7 @@ angular.module('ngAnimate', ['ng']) * * @param {DOMElement} element the element that will be animated * @param {string} className the CSS class that will be animated and then removed from the element + * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ removeClass : function(element, className, options) { @@ -987,6 +1113,7 @@ angular.module('ngAnimate', ['ng']) * @param {string} add the CSS classes which will be added to the element * @param {string} remove the CSS class which will be removed from the element * CSS classes have been set on the element + * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation * @return {Promise} the animation callback promise */ setClass : function(element, add, remove, options) { @@ -997,7 +1124,7 @@ angular.module('ngAnimate', ['ng']) element = stripCommentsFromElement(element); if (classBasedAnimationsBlocked(element)) { - return $delegate.$$setClassImmediately(element, add, remove); + return $delegate.$$setClassImmediately(element, add, remove, options); } // we're using a combined array for both the add and remove @@ -1026,7 +1153,7 @@ angular.module('ngAnimate', ['ng']) if (hasCache) { if (options && cache.options) { - cache.options = cache.options.concat(options); + cache.options = angular.extend(cache.options || {}, options); } //the digest cycle will combine all the animations into one function @@ -1121,9 +1248,8 @@ angular.module('ngAnimate', ['ng']) and the onComplete callback will be fired once the animation is fully complete. */ function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) { - var noopCancel = noop; - var runner = animationRunner(element, animationEvent, className); + var runner = animationRunner(element, animationEvent, className, options); if (!runner) { fireDOMOperation(); fireBeforeCallbackAsync(); @@ -1193,7 +1319,10 @@ angular.module('ngAnimate', ['ng']) } } - if (runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { + if (runner.isClassBased + && !runner.isSetClassOperation + && animationEvent != 'animate' + && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } @@ -1228,8 +1357,8 @@ angular.module('ngAnimate', ['ng']) //the ng-animate class does nothing, but it's here to allow for //parent animations to find and cancel child animations when needed element.addClass(NG_ANIMATE_CLASS_NAME); - if (isArray(options)) { - forEach(options, function(className) { + if (options && options.tempClasses) { + forEach(options.tempClasses, function(className) { element.addClass(className); }); } @@ -1301,9 +1430,13 @@ angular.module('ngAnimate', ['ng']) function closeAnimation() { if (!closeAnimation.hasBeenRun) { + if (runner) { //the runner doesn't exist if it fails to instantiate + runner.applyStyles(); + } + closeAnimation.hasBeenRun = true; - if (isArray(options)) { - forEach(options, function(className) { + if (options && options.tempClasses) { + forEach(options.tempClasses, function(className) { element.removeClass(className); }); } @@ -1594,7 +1727,7 @@ angular.module('ngAnimate', ['ng']) return parentID + '-' + extractElementNode(element).getAttribute('class'); } - function animateSetup(animationEvent, element, className) { + function animateSetup(animationEvent, element, className, styles) { var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0; var cacheKey = getCacheKey(element); @@ -1626,7 +1759,7 @@ angular.module('ngAnimate', ['ng']) return false; } - var blockTransition = structural && transitionDuration > 0; + var blockTransition = styles || (structural && transitionDuration > 0); var blockAnimation = animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0; @@ -1645,6 +1778,9 @@ angular.module('ngAnimate', ['ng']) if (blockTransition) { blockTransitions(node, true); + if (styles) { + element.css(styles); + } } if (blockAnimation) { @@ -1654,7 +1790,7 @@ angular.module('ngAnimate', ['ng']) return true; } - function animateRun(animationEvent, element, className, activeAnimationComplete) { + function animateRun(animationEvent, element, className, activeAnimationComplete, styles) { var node = extractElementNode(element); var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); if (node.getAttribute('class').indexOf(className) == -1 || !elementData) { @@ -1662,10 +1798,6 @@ angular.module('ngAnimate', ['ng']) return; } - if (elementData.blockTransition) { - blockTransitions(node, false); - } - var activeClassName = ''; var pendingClassName = ''; forEach(className.split(' '), function(klass, i) { @@ -1696,6 +1828,9 @@ angular.module('ngAnimate', ['ng']) if (!staggerTime) { element.addClass(activeClassName); + if (elementData.blockTransition) { + blockTransitions(node, false); + } } var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; @@ -1708,6 +1843,14 @@ angular.module('ngAnimate', ['ng']) return; } + if (!staggerTime && styles) { + if (!timings.transitionDuration) { + element.css('transition', timings.animationDuration + 's linear all'); + appliedStyles.push('transition'); + } + element.css(styles); + } + var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); var maxDelayTime = maxDelay * ONE_SECOND; @@ -1732,11 +1875,24 @@ angular.module('ngAnimate', ['ng']) element.addClass(pendingClassName); staggerTimeout = $timeout(function() { staggerTimeout = null; - element.addClass(activeClassName); - element.removeClass(pendingClassName); + + if (timings.transitionDuration > 0) { + blockTransitions(node, false); + } if (timings.animationDuration > 0) { blockAnimations(node, false); } + + element.addClass(activeClassName); + element.removeClass(pendingClassName); + + if (styles) { + if (timings.transitionDuration === 0) { + element.css('transition', timings.animationDuration + 's linear all'); + } + element.css(styles); + appliedStyles.push('transition'); + } }, staggerTime * ONE_SECOND, false); } @@ -1797,28 +1953,28 @@ angular.module('ngAnimate', ['ng']) node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : ''; } - function animateBefore(animationEvent, element, className, calculationDecorator) { - if (animateSetup(animationEvent, element, className, calculationDecorator)) { + function animateBefore(animationEvent, element, className, styles) { + if (animateSetup(animationEvent, element, className, styles)) { return function(cancelled) { cancelled && animateClose(element, className); }; } } - function animateAfter(animationEvent, element, className, afterAnimationComplete) { + function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) { if (element.data(NG_ANIMATE_CSS_DATA_KEY)) { - return animateRun(animationEvent, element, className, afterAnimationComplete); + return animateRun(animationEvent, element, className, afterAnimationComplete, styles); } else { animateClose(element, className); afterAnimationComplete(); } } - function animate(animationEvent, element, className, animationComplete) { + function animate(animationEvent, element, className, animationComplete, options) { //If the animateSetup function doesn't bother returning a //cancellation function then it means that there is no animation //to perform at all - var preReflowCancellation = animateBefore(animationEvent, element, className); + var preReflowCancellation = animateBefore(animationEvent, element, className, options.from); if (!preReflowCancellation) { clearCacheAfterReflow(); animationComplete(); @@ -1835,7 +1991,7 @@ angular.module('ngAnimate', ['ng']) //once the reflow is complete then we point cancel to //the new cancellation function which will remove all of the //animation properties from the active animation - cancel = animateAfter(animationEvent, element, className, animationComplete); + cancel = animateAfter(animationEvent, element, className, animationComplete, options.to); }); return function(cancelled) { @@ -1857,22 +2013,33 @@ angular.module('ngAnimate', ['ng']) } return { - enter : function(element, animationCompleted) { - return animate('enter', element, 'ng-enter', animationCompleted); + animate : function(element, className, from, to, animationCompleted, options) { + options = options || {}; + options.from = from; + options.to = to; + return animate('animate', element, className, animationCompleted, options); + }, + + enter : function(element, animationCompleted, options) { + options = options || {}; + return animate('enter', element, 'ng-enter', animationCompleted, options); }, - leave : function(element, animationCompleted) { - return animate('leave', element, 'ng-leave', animationCompleted); + leave : function(element, animationCompleted, options) { + options = options || {}; + return animate('leave', element, 'ng-leave', animationCompleted, options); }, - move : function(element, animationCompleted) { - return animate('move', element, 'ng-move', animationCompleted); + move : function(element, animationCompleted, options) { + options = options || {}; + return animate('move', element, 'ng-move', animationCompleted, options); }, - beforeSetClass : function(element, add, remove, animationCompleted) { + beforeSetClass : function(element, add, remove, animationCompleted, options) { + options = options || {}; var className = suffixClasses(remove, '-remove') + ' ' + suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className); + var cancellationMethod = animateBefore('setClass', element, className, options.from); if (cancellationMethod) { afterReflow(element, animationCompleted); return cancellationMethod; @@ -1881,8 +2048,9 @@ angular.module('ngAnimate', ['ng']) animationCompleted(); }, - beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add')); + beforeAddClass : function(element, className, animationCompleted, options) { + options = options || {}; + var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from); if (cancellationMethod) { afterReflow(element, animationCompleted); return cancellationMethod; @@ -1891,8 +2059,9 @@ angular.module('ngAnimate', ['ng']) animationCompleted(); }, - beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove')); + beforeRemoveClass : function(element, className, animationCompleted, options) { + options = options || {}; + var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from); if (cancellationMethod) { afterReflow(element, animationCompleted); return cancellationMethod; @@ -1901,19 +2070,22 @@ angular.module('ngAnimate', ['ng']) animationCompleted(); }, - setClass : function(element, add, remove, animationCompleted) { + setClass : function(element, add, remove, animationCompleted, options) { + options = options || {}; remove = suffixClasses(remove, '-remove'); add = suffixClasses(add, '-add'); var className = remove + ' ' + add; - return animateAfter('setClass', element, className, animationCompleted); + return animateAfter('setClass', element, className, animationCompleted, options.to); }, - addClass : function(element, className, animationCompleted) { - return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted); + addClass : function(element, className, animationCompleted, options) { + options = options || {}; + return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to); }, - removeClass : function(element, className, animationCompleted) { - return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); + removeClass : function(element, className, animationCompleted, options) { + options = options || {}; + return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to); } }; diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 9690b46391c0..773d20d1d442 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -803,7 +803,7 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) }; angular.forEach( - ['enter','leave','move','addClass','removeClass','setClass'], function(method) { + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { animate[method] = function() { animate.queue.push({ event : method, diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js index ff1f15cbed8f..f462b6c86b79 100644 --- a/test/ng/animateSpec.js +++ b/test/ng/animateSpec.js @@ -50,6 +50,15 @@ describe("$animate", function() { expect(element.text()).toBe('21'); })); + it("should apply styles instantly to the element", + inject(function($animate, $compile, $rootScope) { + + $animate.animate(element, { color: 'rgb(0, 0, 0)' }); + expect(element.css('color')).toBe('rgb(0, 0, 0)'); + + $animate.animate(element, { color: 'rgb(255, 0, 0)' }, { color: 'rgb(0, 255, 0)' }); + expect(element.css('color')).toBe('rgb(0, 255, 0)'); + })); it("should still perform DOM operations even if animations are disabled (post-digest)", inject(function($animate, $rootScope) { $animate.enabled(false); @@ -103,6 +112,68 @@ describe("$animate", function() { }); inject(); }); + + it("should apply and retain inline styles on the element that is animated", inject(function($animate, $rootScope) { + var element = jqLite('
'); + var parent = jqLite('
'); + var other = jqLite('
'); + parent.append(other); + $animate.enabled(true); + + $animate.enter(element, parent, null, { + to: { color : 'red' } + }); + assertColor('red'); + + $animate.move(element, null, other, { + to: { color : 'yellow' } + }); + assertColor('yellow'); + + $animate.addClass(element, 'on', { + to: { color : 'green' } + }); + $rootScope.$digest(); + assertColor('green'); + + $animate.setClass(element, 'off', 'on', { + to: { color : 'black' } + }); + $rootScope.$digest(); + assertColor('black'); + + $animate.removeClass(element, 'off', { + to: { color : 'blue' } + }); + $rootScope.$digest(); + assertColor('blue'); + + $animate.leave(element, 'off', { + to: { color : 'blue' } + }); + assertColor('blue'); //nothing should happen the element is gone anyway + + function assertColor(color) { + expect(element[0].style.color).toBe(color); + } + })); + + it("should merge the from and to styles that are provided", + inject(function($animate, $rootScope) { + + var element = jqLite('
'); + + element.css('color', 'red'); + $animate.addClass(element, 'on', { + from : { color : 'green' }, + to : { borderColor : 'purple' } + }); + $rootScope.$digest(); + + var style = element[0].style; + expect(style.color).toBe('green'); + expect(style.borderColor).toBe('purple'); + })); }); describe('CSS class DOM manipulation', function() { diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index 5140df4fbef1..260ba914e44b 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -170,13 +170,13 @@ describe('ngShow / ngHide animations', function() { item = $animate.queue.shift(); expect(item.event).toEqual('addClass'); - expect(item.options).toEqual('ng-hide-animate'); + expect(item.options.tempClasses).toEqual('ng-hide-animate'); $scope.on = true; $scope.$digest(); item = $animate.queue.shift(); expect(item.event).toEqual('removeClass'); - expect(item.options).toEqual('ng-hide-animate'); + expect(item.options.tempClasses).toEqual('ng-hide-animate'); })); }); @@ -217,13 +217,13 @@ describe('ngShow / ngHide animations', function() { item = $animate.queue.shift(); expect(item.event).toEqual('removeClass'); - expect(item.options).toEqual('ng-hide-animate'); + expect(item.options.tempClasses).toEqual('ng-hide-animate'); $scope.on = true; $scope.$digest(); item = $animate.queue.shift(); expect(item.event).toEqual('addClass'); - expect(item.options).toEqual('ng-hide-animate'); + expect(item.options.tempClasses).toEqual('ng-hide-animate'); })); }); }); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index a88f4b773ae9..df5b48f1bf13 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -292,7 +292,7 @@ describe("ngAnimate", function() { }); $animateProvider.register('.custom-delay', function($timeout) { function animate(element, done) { - done = arguments.length == 3 ? arguments[2] : done; + done = arguments.length == 4 ? arguments[2] : done; $timeout(done, 2000, false); return function() { element.addClass('animation-cancelled'); @@ -306,7 +306,7 @@ describe("ngAnimate", function() { }); $animateProvider.register('.custom-long-delay', function($timeout) { function animate(element, done) { - done = arguments.length == 3 ? arguments[2] : done; + done = arguments.length == 4 ? arguments[2] : done; $timeout(done, 20000, false); return function(cancelled) { element.addClass(cancelled ? 'animation-cancelled' : 'animation-ended'); @@ -329,7 +329,7 @@ describe("ngAnimate", function() { return function($animate, $compile, $rootScope, $rootElement) { element = $compile('
')($rootScope); - forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) { + forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move', '.my-inline-animation'], function(selector) { ss.addRule(selector, '-webkit-transition:1s linear all;' + 'transition:1s linear all;'); }); @@ -454,6 +454,20 @@ describe("ngAnimate", function() { expect(element.text()).toBe('21'); })); + it("should perform the animate event", + inject(function($animate, $compile, $rootScope, $timeout, $sniffer) { + + $rootScope.$digest(); + $animate.animate(element, { color: 'rgb(255, 0, 0)' }, { color: 'rgb(0, 0, 255)' }, 'animated'); + $rootScope.$digest(); + + if($sniffer.transitions) { + expect(element.css('color')).toBe('rgb(255, 0, 0)'); + $animate.triggerReflow(); + } + expect(element.css('color')).toBe('rgb(0, 0, 255)'); + })); + it("should animate the show animation event", inject(function($animate, $rootScope, $sniffer) { @@ -653,6 +667,16 @@ describe("ngAnimate", function() { expect(child.attr('class')).toContain('ng-hide-remove-active'); browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + //animate + $animate.animate(child, null, null, 'my-inline-animation'); + $rootScope.$digest(); + $animate.triggerReflow(); + + expect(child.attr('class')).toContain('my-inline-animation'); + expect(child.attr('class')).toContain('my-inline-animation-active'); + browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + $animate.triggerCallbackPromise(); + //leave $animate.leave(child); $rootScope.$digest(); @@ -1037,8 +1061,124 @@ describe("ngAnimate", function() { expect(element.hasClass('custom-long-delay-add')).toBe(false); expect(element.hasClass('custom-long-delay-add-active')).toBe(false); })); + + it('should apply directive styles and provide the style collection to the animation function', function() { + var animationDone; + var animationStyles; + var proxyAnimation = function() { + var limit = arguments.length-1; + animationStyles = arguments[limit]; + animationDone = arguments[limit-1]; + }; + module(function($animateProvider) { + $animateProvider.register('.capture', function() { + return { + enter : proxyAnimation, + leave : proxyAnimation, + move : proxyAnimation, + addClass : proxyAnimation, + removeClass : proxyAnimation, + setClass : proxyAnimation + }; + }); + }); + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) { + $rootElement = _$rootElement_; + + $animate.enabled(true); + + element = $compile(html('
'))($rootScope); + var otherParent = $compile('
')($rootScope); + var child = $compile('
')($rootScope); + + $rootElement.append(otherParent); + $rootScope.$digest(); + + var styles = { + from: { backgroundColor: 'blue' }, + to: { backgroundColor: 'red' } + }; + + //enter + $animate.enter(child, element, null, styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + //move + $animate.move(child, null, otherParent, styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + //addClass + $animate.addClass(child, 'on', styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + //setClass + $animate.setClass(child, 'off', 'on', styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + //removeClass + $animate.removeClass(child, 'off', styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + //leave + $animate.leave(child, styles); + $rootScope.$digest(); + $animate.triggerReflow(); + expect(animationStyles).toEqual(styles); + animationDone(); + animationDone = animationStyles = null; + $animate.triggerCallbacks(); + + dealoc(otherParent); + }); + }); }); + it("should apply animated styles even if there are no detected animations", + inject(function($compile, $animate, $rootScope, $sniffer, $rootElement, $document) { + + $animate.enabled(true); + jqLite($document[0].body).append($rootElement); + + element = $compile('
')($rootScope); + + $animate.enter(element, $rootElement, null, { + to : {borderColor: 'red'} + }); + + $rootScope.$digest(); + expect(element).toHaveClass('ng-animate'); + + $animate.triggerReflow(); + $animate.triggerCallbacks(); + + expect(element).not.toHaveClass('ng-animate'); + expect(element.attr('style')).toMatch(/border-color: red/); + })); describe("with CSS3", function() { @@ -1218,6 +1358,28 @@ describe("ngAnimate", function() { }) ); + it("should piggy-back-transition the styles with the max keyframe duration if provided by the directive", + inject(function($compile, $animate, $rootScope, $sniffer) { + + $animate.enabled(true); + ss.addRule('.on', '-webkit-animation: 1s keyframeanimation; animation: 1s keyframeanimation;'); + + element = $compile(html('
1
'))($rootScope); + + $animate.addClass(element, 'on', { + to: {borderColor: 'blue'} + }); + + $rootScope.$digest(); + if ($sniffer.transitions) { + $animate.triggerReflow(); + expect(element.attr('style')).toContain('border-color: blue'); + expect(element.attr('style')).toMatch(/transition:.*1s/); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + } + + expect(element.attr('style')).toContain('border-color: blue'); + })); it("should pause the playstate when performing a stagger animation", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { @@ -1372,6 +1534,86 @@ describe("ngAnimate", function() { } })); + it("should stagger items and apply the transition + directive styles the right time when piggy-back styles are used", + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) { + + if(!$sniffer.transitions) return; + + $animate.enabled(true); + + ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave', + '-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' + + 'animation:my_animation 1s 1s, your_animation 1s 2s;'); + + ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger', + '-webkit-animation-delay:0.1s;' + + 'animation-delay:0.1s;'); + + var styles = { + from : { left : '50px' }, + to : { left : '100px' } + }; + var container = $compile(html('
'))($rootScope); + + var elements = []; + for(var i = 0; i < 4; i++) { + var newScope = $rootScope.$new(); + var element = $compile('
')(newScope); + $animate.enter(element, container, null, styles); + elements.push(element); + } + + $rootScope.$digest(); + + for(i = 0; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter'); + assertTransitionDuration(elements[i], '2', true); + assertLeftStyle(elements[i], '50'); + } + + $animate.triggerReflow(); + + expect(elements[0]).toHaveClass('ng-enter-active'); + assertLeftStyle(elements[0], '100'); + assertTransitionDuration(elements[0], '1'); + + for(i = 1; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter-active'); + assertTransitionDuration(elements[i], '1', true); + assertLeftStyle(elements[i], '100', true); + } + + $timeout.flush(300); + + for(i = 1; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter-active'); + assertTransitionDuration(elements[i], '1'); + assertLeftStyle(elements[i], '100'); + } + + $timeout.flush(); + + for(i = 0; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter'); + expect(elements[i]).not.toHaveClass('ng-enter-active'); + assertTransitionDuration(elements[i], '1', true); + assertLeftStyle(elements[i], '100'); + } + + function assertLeftStyle(element, val, not) { + var regex = new RegExp('left: ' + val + 'px'); + var style = element.attr('style'); + not ? expect(style).not.toMatch(regex) + : expect(style).toMatch(regex); + } + + function assertTransitionDuration(element, val, not) { + var regex = new RegExp('transition:.*' + val + 's'); + var style = element.attr('style'); + not ? expect(style).not.toMatch(regex) + : expect(style).toMatch(regex); + } + })); }); @@ -1741,6 +1983,82 @@ describe("ngAnimate", function() { } })); + it("should stagger items, apply directive styles but not apply a transition style when the stagger step kicks in", + inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) { + + if(!$sniffer.transitions) return; + + $animate.enabled(true); + + ss.addRule('.stagger-animation.ng-enter, .ani.ng-leave', + '-webkit-transition:1s linear color 2s, 3s linear font-size 4s;' + + 'transition:1s linear color 2s, 3s linear font-size 4s;'); + + ss.addRule('.stagger-animation.ng-enter-stagger, .ani.ng-leave-stagger', + '-webkit-transition-delay:0.1s;' + + 'transition-delay:0.1s;'); + + var styles = { + from : { left : '155px' }, + to : { left : '255px' } + }; + var container = $compile(html('
'))($rootScope); + + var elements = []; + for(var i = 0; i < 4; i++) { + var newScope = $rootScope.$new(); + var element = $compile('
')(newScope); + $animate.enter(element, container, null, styles); + elements.push(element); + } + + $rootScope.$digest(); + + for(i = 0; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter'); + assertLeftStyle(elements[i], '155'); + } + + $animate.triggerReflow(); + + expect(elements[0]).toHaveClass('ng-enter-active'); + assertLeftStyle(elements[0], '255'); + assertNoTransitionDuration(elements[0]); + + for(i = 1; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter-active'); + assertLeftStyle(elements[i], '255', true); + } + + $timeout.flush(300); + + for(i = 1; i < 4; i++) { + expect(elements[i]).toHaveClass('ng-enter-active'); + assertNoTransitionDuration(elements[i]); + assertLeftStyle(elements[i], '255'); + } + + $timeout.flush(); + + for(i = 0; i < 4; i++) { + expect(elements[i]).not.toHaveClass('ng-enter'); + expect(elements[i]).not.toHaveClass('ng-enter-active'); + assertNoTransitionDuration(elements[i]); + assertLeftStyle(elements[i], '255'); + } + + function assertLeftStyle(element, val, not) { + var regex = new RegExp('left: ' + val + 'px'); + var style = element.attr('style'); + not ? expect(style).not.toMatch(regex) + : expect(style).toMatch(regex); + } + + function assertNoTransitionDuration(element) { + var style = element.attr('style'); + expect(style).not.toMatch(/transition/); + } + })); it("should apply a closing timeout to close all pending transitions", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { @@ -2042,6 +2360,29 @@ describe("ngAnimate", function() { expect(elements[i].attr('style')).toBeFalsy(); } })); + + it("should create a piggy-back-transition which has a duration the same as the max keyframe duration if any directive styles are provided", + inject(function($compile, $animate, $rootScope, $sniffer) { + + $animate.enabled(true); + ss.addRule('.on', '-webkit-transition: 1s linear all; transition: 1s linear all;'); + + element = $compile(html('
1
'))($rootScope); + + $animate.addClass(element, 'on', { + to: {color: 'red'} + }); + + $rootScope.$digest(); + if ($sniffer.transitions) { + $animate.triggerReflow(); + expect(element.attr('style')).toContain('color: red'); + expect(element.attr('style')).not.toContain('transition'); + browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); + } + + expect(element.attr('style')).toContain('color: red'); + })); }); @@ -2472,9 +2813,9 @@ describe("ngAnimate", function() { }; function capture(event) { - return function(element, add, remove, done) { + return function(element, add, remove, styles, done) { //some animations only have one extra param - done = done || remove || add; + done = arguments[arguments.length-2]; //the last one is the styles array captures[event]=done; }; } @@ -2491,28 +2832,40 @@ describe("ngAnimate", function() { $compile(element)($rootScope); assertTempClass('enter', 'temp-enter', function() { - $animate.enter(element, container, null, 'temp-enter'); + $animate.enter(element, container, null, { + tempClasses: 'temp-enter' + }); }); assertTempClass('move', 'temp-move', function() { - $animate.move(element, null, container2, 'temp-move'); + $animate.move(element, null, container2, { + tempClasses: 'temp-move' + }); }); assertTempClass('addClass', 'temp-add', function() { - $animate.addClass(element, 'add', 'temp-add'); + $animate.addClass(element, 'add', { + tempClasses: 'temp-add' + }); }); assertTempClass('removeClass', 'temp-remove', function() { - $animate.removeClass(element, 'add', 'temp-remove'); + $animate.removeClass(element, 'add', { + tempClasses: 'temp-remove' + }); }); element.addClass('remove'); assertTempClass('setClass', 'temp-set', function() { - $animate.setClass(element, 'add', 'remove', 'temp-set'); + $animate.setClass(element, 'add', 'remove', { + tempClasses: 'temp-set' + }); }); assertTempClass('leave', 'temp-leave', function() { - $animate.leave(element, 'temp-leave'); + $animate.leave(element, { + tempClasses: 'temp-leave' + }); }); function assertTempClass(event, className, animationOperation) {