diff options
Diffstat (limited to 'src/ngAnimate')
| -rw-r--r-- | src/ngAnimate/animate.js | 714 |
1 files changed, 714 insertions, 0 deletions
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js new file mode 100644 index 00000000..ab7a19b0 --- /dev/null +++ b/src/ngAnimate/animate.js @@ -0,0 +1,714 @@ +/** + * @ngdoc overview + * @name ngAnimate + * @description + * + * ngAnimate + * ========= + * + * The ngAnimate module is an optional module that comes packed with AngularJS that can be included within an AngularJS + * application to provide support for CSS and JavaScript animation hooks. + * + * To make use of animations with AngularJS, the `angular-animate.js` JavaScript file must be included into your application + * and the `ngAnimate` module must be included as a dependency. + * + * <pre> + * angular.module('App', ['ngAnimate']); + * </pre> + * + * Then, to see animations in action, all that is required is to define the appropriate CSS classes + * or to register a JavaScript animation via the $animation service. The directives that support animation automatically are: + * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView`. Custom directives can take advantage of animation + * by using the `$animate` service. + * + * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: + * + * | Directive | Supported Animations | + * |========================================================== |====================================================| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngShow#animations ngShow & ngHide} | show and hide | + * | {@link ng.directive:ngShow#animations ngClass} | add and remove | + * + * You can find out more information about animations upon visiting each directive page. + * + * Below is an example of how to apply animations to a directive that supports animation hooks: + * + * <pre> + * <style type="text/css"> + * .slide.ng-enter > div, + * .slide.ng-leave > div { + * -webkit-transition:0.5s linear all; + * -moz-transition:0.5s linear all; + * -o-transition:0.5s linear all; + * transition:0.5s linear all; + * } + * + * .slide > .ng-enter { } /* starting animations for enter */ + * .slide > .ng-enter-active { } /* terminal animations for enter */ + * .slide > .ng-leave { } /* starting animations for leave */ + * .slide > .ng-leave-active { } /* terminal animations for leave */ + * </style> + * + * <!-- + * the animate service will automatically add .ng-enter and .ng-leave to the element + * to trigger the CSS animations + * --> + * <ANY class="slide" ng-include="..."></ANY> + * </pre> + * + * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's + * animation has completed. + * + * <h2>CSS-defined Animations</h2> + * 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. + * + * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: + * + * <pre> + * <style type="text/css"> + * /* + * The animate class is apart of the element and the ng-enter class + * is attached to the element once the enter animation event is triggered + * */ + * .reveal-animation.ng-enter { + * -webkit-transition: 1s linear all; /* Safari/Chrome */ + * -moz-transition: 1s linear all; /* Firefox */ + * -o-transition: 1s linear all; /* Opera */ + * transition: 1s linear all; /* IE10+ and Future Browsers */ + * + * /* The animation preparation code */ + * opacity: 0; + * } + * + * /* + * Keep in mind that you want to combine both CSS + * classes together to avoid any CSS-specificity + * conflicts + * */ + * .reveal-animation.ng-enter.ng-enter-active { + * /* The animation code itself */ + * opacity: 1; + * } + * </style> + * + * <div class="view-container"> + * <div ng-view class="reveal-animation"></div> + * </div> + * </pre> + * + * The following code below demonstrates how to perform animations using **CSS animations** with Angular: + * + * <pre> + * <style type="text/css"> + * .reveal-animation.ng-enter { + * -webkit-animation: enter_sequence 1s linear; /* Safari/Chrome */ + * -moz-animation: enter_sequence 1s linear; /* Firefox */ + * -o-animation: enter_sequence 1s linear; /* Opera */ + * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ + * } + * @-webkit-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @-moz-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @-o-keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * @keyframes enter_sequence { + * from { opacity:0; } + * to { opacity:1; } + * } + * </style> + * + * <div class="view-container"> + * <div ng-view class="reveal-animation"></div> + * </div> + * </pre> + * + * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. + * + * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add + * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically + * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be + * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end + * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element + * has no CSS transition/animation classes applied to it. + * + * <h2>JavaScript-defined Animations</h2> + * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not + * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. + * + * <pre> + * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. + * var ngModule = angular.module('YourApp', []); + * ngModule.animation('.my-crazy-animation', function() { + * return { + * enter: function(element, done) { + * //run the animation + * //!annotate Cancel Animation|This function (if provided) will perform the cancellation of the animation when another is triggered + * return function(element, done) { + * //cancel the animation + * } + * } + * leave: function(element, done) { }, + * move: function(element, done) { }, + * show: function(element, done) { }, + * hide: function(element, done) { }, + * addClass: function(element, className, done) { }, + * removeClass: function(element, className, done) { }, + * } + * }); + * </pre> + * + * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run + * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits + * the element's CSS class attribute value and then run the matching animation event function (if found). + * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function + * be executed. It should be also noted that only simple or compound class selectors are allowed. + * + * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. + * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, + * 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). + * + */ + +angular.module('ngAnimate', ['ng']) + + /** + * @ngdoc object + * @name ngAnimate.$animateProvider + * @description + * + * The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside + * of a module. When an animation is triggered, the $animate service will query the $animation function to find any + * animations that match the provided name value. + * + * Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application. + * + */ + .config(['$provide', '$animateProvider', function($provide, $animateProvider) { + var selectors = $animateProvider.$$selectors; + + var NG_ANIMATE_STATE = '$$ngAnimateState'; + var rootAnimateState = {running:true}; + + $provide.decorator('$animate', ['$delegate', '$injector', '$window', '$sniffer', '$rootElement', + function($delegate, $injector, $window, $sniffer, $rootElement) { + + var noop = angular.noop; + var forEach = angular.forEach; + + $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); + + function lookup(name) { + if (name) { + var classes = name.substr(1).split('.'), + classMap = {}; + + for (var i = 0, ii = classes.length; i < ii; i++) { + classMap[classes[i]] = true; + } + + var matches = []; + for (var i = 0, ii = selectors.length; i < ii; i++) { + var selectorFactory = selectors[i]; + var found = true; + for(var j = 0, jj = selectorFactory.selectors.length; j < jj; j++) { + var klass = selectorFactory.selectors[j]; + if(klass.length > 0) { + found = found && classMap[klass]; + } + } + if(found) { + matches.push($injector.get(selectorFactory.name)); + } + } + return matches; + } + }; + + /** + * @ngdoc object + * @name ngAnimate.$animate + * @requires $window, $sniffer, $rootElement + * @function + * + * @description + * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) + * as well as during addClass and removeClass operations. When any of these operations are run, the $animate service + * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) + * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. + * + * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives + * will work out of the box without any extra configuration. + * + * Please visit the {@link ngAnimate ngAnimate} module overview page learn more about how to use animations in your application. + * + */ + return { + /** + * @ngdoc function + * @name ngAnimate.$animate#enter + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Appends the element to the parent element that resides in the document and then runs the enter animation. Once + * the animation is started, the following CSS classes will be present on the element for the duration of the animation: + * <pre> + * .ng-enter + * .ng-enter-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + enter : function(element, parent, after, done) { + $delegate.enter(element, parent, after); + performAnimation('enter', 'ng-enter', element, parent, after, done); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#leave + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once + * the animation is started, the following CSS classes will be added for the duration of the animation: + * <pre> + * .ng-leave + * .ng-leave-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + leave : function(element, done) { + performAnimation('leave', 'ng-leave', element, null, null, function() { + $delegate.leave(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#move + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parent container or + * add the element directly after the after element if present. Then the move animation will be run. Once + * the animation is started, the following CSS classes will be added for the duration of the animation: + * <pre> + * .ng-move + * .ng-move-active + * </pre> + * + * Once the animation is complete then the done callback, if provided, will be also fired. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the move animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation + * @param {function()=} done callback function that will be called once the animation is complete + */ + move : function(element, parent, after, done) { + $delegate.move(element, parent, after); + performAnimation('move', 'ng-move', element, null, null, done); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#show + * @methodOf ngAnimate.$animate + * @function + * + * @description + * Reveals the element by removing the `ng-hide` class thus performing an animation in the process. During + * this animation the CSS classes present on the element will be: + * + * <pre> + * .ng-hide //already on the element if hidden + * .ng-hide-remove + * .ng-hide-remove-active + * </pre> + * + * Once the animation is complete then all three CSS classes will be removed from the element. + * The done callback, if provided, will be also fired once the animation is complete. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + * @param {function()=} done callback function that will be called once the animation is complete + */ + show : function(element, done) { + performAnimation('show', 'ng-hide-remove', element, null, null, function() { + $delegate.show(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#hide + * @methodOf ngAnimate.$animate + * + * @description + * Sets the element to hidden by adding the `ng-hide` class it. However, before the class is applied + * the following CSS classes will be added temporarily to trigger any animation code: + * + * <pre> + * .ng-hide-add + * .ng-hide-add-active + * </pre> + * + * Once the animation is complete then both CSS classes will be removed and `ng-hide` will be added to the element. + * The done callback, if provided, will be also fired once the animation is complete. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + * @param {function()=} done callback function that will be called once the animation is complete + */ + hide : function(element, done) { + performAnimation('hide', 'ng-hide-add', element, null, null, function() { + $delegate.hide(element, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#addClass + * @methodOf ngAnimate.$animate + * + * @description + * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. + * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide + * the animate service the setup and active CSS classes in order to trigger the animation. + * + * For example, upon execution of: + * + * <pre> + * $animate.addClass(element, 'super'); + * </pre> + * + * The generated CSS class values present on element will look like: + * <pre> + * .super-add + * .super-add-active + * </pre> + * + * And upon completion, the generated animation CSS classes will be removed from the element, but the className + * value will be attached to the element. In this case, based on the previous example, the resulting CSS class for the element + * will look like so: + * + * <pre> + * .super + * </pre> + * + * Once this is complete, then the done callback, if provided, will be fired. + * + * @param {jQuery/jqLite element} element the element that will be animated + * @param {string} className the CSS class that will be animated and then attached to the element + * @param {function()=} done callback function that will be called once the animation is complete + */ + addClass : function(element, className, done) { + performAnimation('addClass', className, element, null, null, function() { + $delegate.addClass(element, className, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#removeClass + * @methodOf ngAnimate.$animate + * + * @description + * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value + * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in + * order to provide the animate service the setup and active CSS classes in order to trigger the animation. + * + * For example, upon the execution of: + * + * <pre> + * $animate.removeClass(element, 'super'); + * </pre> + * + * The CSS class values present on element during the animation will look like: + * + * <pre> + * .super //this was here from before + * .super-remove + * .super-remove-active + * </pre> + * + * And upon completion, the generated animation CSS classes will be removed from the element as well as the + * className value that was provided (in this case {@type super} will be removed). Once that is complete, then, if provided, + * the done callback will be fired. + * + * @param {jQuery/jqLite element} element the element that will be animated + * @param {string} className the CSS class that will be animated and then removed from the element + * @param {function()=} done callback function that will be called once the animation is complete + */ + removeClass : function(element, className, done) { + performAnimation('removeClass', className, element, null, null, function() { + $delegate.removeClass(element, className, done); + }); + }, + + /** + * @ngdoc function + * @name ngAnimate.$animate#enabled + * @methodOf ngAnimate.$animate + * @function + * + * @param {boolean=} If provided then set the animation on or off. + * @return {boolean} Current animation state. + * + * @description + * Globally enables/disables animations. + * + */ + enabled : function(value) { + if (arguments.length) { + rootAnimateState.running = !value; + } + return !rootAnimateState.running; + } + }; + + /* + all animations call this shared animation triggering function internally. + The event variable refers to the JavaScript animation event that will be triggered + and the className value is the name of the animation that will be applied within the + CSS code. Element, parent and after are provided DOM elements for the animation + 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)) { + (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 + }); + }); + + 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)(); + return; + } + + var animationData = element.data(NG_ANIMATE_STATE) || {}; + + //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(); + } + + element.data(NG_ANIMATE_STATE, { + running:true, + animations:animations, + done:done + }); + + if(event == 'addClass') { + className = suffixClasses(className, '-add'); + } else if(event == 'removeClass') { + className = suffixClasses(className, '-remove'); + } + + 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); + } + } else { + fn(); + } + }); + } + + function nothingToAnimate(className, element) { + return !(className && className.length > 0 && element.length > 0); + } + + function cancelAnimations(animations) { + forEach(animations, function(animation) { + (animation.cancel || noop)(element); + }); + } + + function suffixClasses(classes, suffix) { + var className = ''; + classes = angular.isArray(classes) ? classes : classes.split(/\s+/); + forEach(classes, function(klass, i) { + if(klass && klass.length > 0) { + className += (i > 0 ? ' ' : '') + klass + suffix; + } + }); + return className; + } + + function progress(index) { + animations[index].done = true; + for(var i=0;i<animations.length;i++) { + if(!animations[i].done) return; + } + done(); + }; + + function done() { + if(!done.hasBeenRun) { + done.hasBeenRun = true; + element.removeClass(className); + element.removeData(NG_ANIMATE_STATE); + (onComplete || noop)(); + } + } + } + }]); + }]) + + .animation('', ['$window','$sniffer', function($window, $sniffer) { + return { + enter : function(element, done) { + return animate(element, 'ng-enter', done); + }, + leave : function(element, done) { + return animate(element, 'ng-leave', done); + }, + move : function(element, done) { + return animate(element, 'ng-move', done); + }, + show : function(element, done) { + return animate(element, 'ng-hide-remove', done); + }, + hide : function(element, done) { + return animate(element, 'ng-hide-add', done); + }, + addClass : function(element, className, done) { + return animate(element, className, done); + }, + removeClass : function(element, className, done) { + return animate(element, className, done); + } + }; + + function animate(element, className, done) { + if (!($sniffer.transitions || $sniffer.animations)) { + done(); + } else { + var activeClassName = ''; + $window.setTimeout(startAnimation, 1); + + //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; + } + + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; + } + + function startAnimation() { + var duration = 0; + forEach(className.split(' '), function(klass, i) { + activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + }); + + element.addClass(activeClassName); + + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; + + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount'; + + //we want all the styles defined before and after + var ELEMENT_NODE = 1; + forEach(element, function(element) { + if (element.nodeType == ELEMENT_NODE) { + var elementStyles = $window.getComputedStyle(element) || {}; + + var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), + parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + + var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), + parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); + + var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), + parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); + + var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), + parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + + if(animationDuration > 0) { + animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, + 1); + } + + duration = Math.max(animationDelay + animationDuration, + transitionDelay + transitionDuration, + duration); + } + }); + + $window.setTimeout(onComplete, duration * 1000); + } + + function onComplete() { + element.removeClass(activeClassName); + done(); + }; + }; + }]); |
