diff options
Diffstat (limited to 'src/ngAnimate/animate.js')
| -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(); +      }; +    }; +  }]); | 
