diff options
Diffstat (limited to 'libs/bower_components/angular-animate/angular-animate.js')
-rw-r--r-- | libs/bower_components/angular-animate/angular-animate.js | 1439 |
1 files changed, 864 insertions, 575 deletions
diff --git a/libs/bower_components/angular-animate/angular-animate.js b/libs/bower_components/angular-animate/angular-animate.js index fc0e217f7e..7c0677e261 100644 --- a/libs/bower_components/angular-animate/angular-animate.js +++ b/libs/bower_components/angular-animate/angular-animate.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.4.3 + * @license AngularJS v1.4.10 * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ @@ -7,6 +7,7 @@ /* jshint ignore:start */ var noop = angular.noop; +var copy = angular.copy; var extend = angular.extend; var jqLite = angular.element; var forEach = angular.forEach; @@ -21,13 +22,63 @@ var isElement = angular.isElement; var ELEMENT_NODE = 1; var COMMENT_NODE = 8; +var ADD_CLASS_SUFFIX = '-add'; +var REMOVE_CLASS_SUFFIX = '-remove'; +var EVENT_CLASS_PREFIX = 'ng-'; +var ACTIVE_CLASS_SUFFIX = '-active'; +var PREPARE_CLASS_SUFFIX = '-prepare'; + var NG_ANIMATE_CLASSNAME = 'ng-animate'; var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; +// Detect proper transitionend/animationend event names. +var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; + +// If unprefixed events are not supported but webkit-prefixed are, use the latter. +// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. +// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` +// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. +// Register both events in case `window.onanimationend` is not supported because of that, +// do the same for `transitionend` as Safari is likely to exhibit similar behavior. +// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit +// therefore there is no reason to test anymore for other vendor prefixes: +// http://caniuse.com/#search=transition +if (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) { + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; +} else { + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; +} + +if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { + CSS_PREFIX = '-webkit-'; + ANIMATION_PROP = 'WebkitAnimation'; + ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; +} else { + ANIMATION_PROP = 'animation'; + ANIMATIONEND_EVENT = 'animationend'; +} + +var DURATION_KEY = 'Duration'; +var PROPERTY_KEY = 'Property'; +var DELAY_KEY = 'Delay'; +var TIMING_KEY = 'TimingFunction'; +var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; +var ANIMATION_PLAYSTATE_KEY = 'PlayState'; +var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; + +var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; +var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; +var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; +var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; + var isPromiseLike = function(p) { return p && p.then ? true : false; -} +}; +var ngMinErr = angular.$$minErr('ng'); function assertArg(arg, name, reason) { if (!arg) { throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); @@ -172,13 +223,29 @@ function applyAnimationToStyles(element, options) { } } -function mergeAnimationOptions(element, target, newOptions) { +function mergeAnimationDetails(element, oldAnimation, newAnimation) { + var target = oldAnimation.options || {}; + var newOptions = newAnimation.options || {}; + var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || ''); var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || ''); var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove); + if (newOptions.preparationClasses) { + target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses); + delete newOptions.preparationClasses; + } + + // noop is basically when there is no callback; otherwise something has been set + var realDomOperation = target.domOperation !== noop ? target.domOperation : null; + extend(target, newOptions); + // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this. + if (realDomOperation) { + target.domOperation = realDomOperation; + } + if (classes.addClass) { target.addClass = classes.addClass; } else { @@ -191,6 +258,9 @@ function mergeAnimationOptions(element, target, newOptions) { target.removeClass = null; } + oldAnimation.addClass = target.addClass; + oldAnimation.removeClass = target.removeClass; + return target; } @@ -256,18 +326,75 @@ function getDomNode(element) { return (element instanceof angular.element) ? element[0] : element; } +function applyGeneratedPreparationClasses(element, event, options) { + var classes = ''; + if (event) { + classes = pendClasses(event, EVENT_CLASS_PREFIX, true); + } + if (options.addClass) { + classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX)); + } + if (options.removeClass) { + classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX)); + } + if (classes.length) { + options.preparationClasses = classes; + element.addClass(classes); + } +} + +function clearGeneratedClasses(element, options) { + if (options.preparationClasses) { + element.removeClass(options.preparationClasses); + options.preparationClasses = null; + } + if (options.activeClasses) { + element.removeClass(options.activeClasses); + options.activeClasses = null; + } +} + +function blockTransitions(node, duration) { + // we use a negative delay value since it performs blocking + // yet it doesn't kill any existing transitions running on the + // same element which makes this safe for class-based animations + var value = duration ? '-' + duration + 's' : ''; + applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]); + return [TRANSITION_DELAY_PROP, value]; +} + +function blockKeyframeAnimations(node, applyBlock) { + var value = applyBlock ? 'paused' : ''; + var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; + applyInlineStyle(node, [key, value]); + return [key, value]; +} + +function applyInlineStyle(node, styleTuple) { + var prop = styleTuple[0]; + var value = styleTuple[1]; + node.style[prop] = value; +} + +function concatWithSpace(a,b) { + if (!a) return b; + if (!b) return a; + return a + ' ' + b; +} + var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { - var tickQueue = []; - var cancelFn; + var queue, cancelFn; function scheduler(tasks) { // we make a copy since RAFScheduler mutates the state // of the passed in array variable and this would be difficult // to track down on the outside code - tickQueue.push([].concat(tasks)); + queue = queue.concat(tasks); nextTick(); } + queue = scheduler.queue = []; + /* waitUntilQuiet does two things: * 1. It will run the FINAL `fn` value only when an uncancelled RAF has passed through * 2. It will delay the next wave of tasks from running until the quiet `fn` has run. @@ -289,17 +416,12 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { return scheduler; function nextTick() { - if (!tickQueue.length) return; + if (!queue.length) return; - var updatedQueue = []; - for (var i = 0; i < tickQueue.length; i++) { - var innerQueue = tickQueue[i]; - runNextTask(innerQueue); - if (innerQueue.length) { - updatedQueue.push(innerQueue); - } + var items = queue.shift(); + for (var i = 0; i < items.length; i++) { + items[i](); } - tickQueue = updatedQueue; if (!cancelFn) { $$rAF(function() { @@ -307,27 +429,109 @@ var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { }); } } - - function runNextTask(tasks) { - var nextTask = tasks.shift(); - nextTask(); - } }]; -var $$AnimateChildrenDirective = [function() { - return function(scope, element, attrs) { - var val = attrs.ngAnimateChildren; - if (angular.isString(val) && val.length === 0) { //empty attribute - element.data(NG_ANIMATE_CHILDREN_DATA, true); - } else { - attrs.$observe('ngAnimateChildren', function(value) { +/** + * @ngdoc directive + * @name ngAnimateChildren + * @restrict AE + * @element ANY + * + * @description + * + * ngAnimateChildren allows you to specify that children of this element should animate even if any + * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` + * (structural) animation, child elements that also have an active structural animation are not animated. + * + * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). + * + * + * @param {string} ngAnimateChildren If the value is empty, `true` or `on`, + * then child animations are allowed. If the value is `false`, child animations are not allowed. + * + * @example + * <example module="ngAnimateChildren" name="ngAnimateChildren" deps="angular-animate.js" animations="true"> + <file name="index.html"> + <div ng-controller="mainController as main"> + <label>Show container? <input type="checkbox" ng-model="main.enterElement" /></label> + <label>Animate children? <input type="checkbox" ng-model="main.animateChildren" /></label> + <hr> + <div ng-animate-children="{{main.animateChildren}}"> + <div ng-if="main.enterElement" class="container"> + List of items: + <div ng-repeat="item in [0, 1, 2, 3]" class="item">Item {{item}}</div> + </div> + </div> + </div> + </file> + <file name="animations.css"> + + .container.ng-enter, + .container.ng-leave { + transition: all ease 1.5s; + } + + .container.ng-enter, + .container.ng-leave-active { + opacity: 0; + } + + .container.ng-leave, + .container.ng-enter-active { + opacity: 1; + } + + .item { + background: firebrick; + color: #FFF; + margin-bottom: 10px; + } + + .item.ng-enter, + .item.ng-leave { + transition: transform 1.5s ease; + } + + .item.ng-enter { + transform: translateX(50px); + } + + .item.ng-enter-active { + transform: translateX(0); + } + </file> + <file name="script.js"> + angular.module('ngAnimateChildren', ['ngAnimate']) + .controller('mainController', function() { + this.animateChildren = false; + this.enterElement = false; + }); + </file> + </example> + */ +var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { + return { + link: function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN_DATA, true); + } else { + // Interpolate and set the value, so that it is available to + // animations that run right after compilation + setData($interpolate(val)(scope)); + attrs.$observe('ngAnimateChildren', setData); + } + + function setData(value) { value = value === 'on' || value === 'true'; element.data(NG_ANIMATE_CHILDREN_DATA, value); - }); + } } }; }]; +var ANIMATE_TIMER_KEY = '$$animateCss'; + /** * @ngdoc service * @name $animateCss @@ -512,8 +716,10 @@ var $$AnimateChildrenDirective = [function() { * * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) + * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and + * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). - * * `transition` - The raw CSS transition style that will be used (e.g. `1s linear all`). + * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. @@ -528,63 +734,23 @@ var $$AnimateChildrenDirective = [function() { * * `stagger` - A numeric time value representing the delay between successively animated elements * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a - * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) - * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.) + * * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) + * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.) + * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once + * the animation is closed. This is useful for when the styles are used purely for the sake of + * the animation and do not have a lasting visual effect on the element (e.g. a colapse and open animation). + * By default this value is set to `false`. * * @return {object} an object with start and end methods and details about the animation. * * * `start` - The method to start the animation. This will return a `Promise` when called. * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. */ - -// Detect proper transitionend/animationend event names. -var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; - -// If unprefixed events are not supported but webkit-prefixed are, use the latter. -// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. -// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` -// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. -// Register both events in case `window.onanimationend` is not supported because of that, -// do the same for `transitionend` as Safari is likely to exhibit similar behavior. -// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit -// therefore there is no reason to test anymore for other vendor prefixes: -// http://caniuse.com/#search=transition -if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { - CSS_PREFIX = '-webkit-'; - TRANSITION_PROP = 'WebkitTransition'; - TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; -} else { - TRANSITION_PROP = 'transition'; - TRANSITIONEND_EVENT = 'transitionend'; -} - -if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { - CSS_PREFIX = '-webkit-'; - ANIMATION_PROP = 'WebkitAnimation'; - ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; -} else { - ANIMATION_PROP = 'animation'; - ANIMATIONEND_EVENT = 'animationend'; -} - -var DURATION_KEY = 'Duration'; -var PROPERTY_KEY = 'Property'; -var DELAY_KEY = 'Delay'; -var TIMING_KEY = 'TimingFunction'; -var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; -var ANIMATION_PLAYSTATE_KEY = 'PlayState'; -var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; -var CLOSING_TIME_BUFFER = 1.5; var ONE_SECOND = 1000; var BASE_TEN = 10; -var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; - -var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; -var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; - -var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; -var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; +var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; +var CLOSING_TIME_BUFFER = 1.5; var DETECT_CSS_PROPERTIES = { transitionDuration: TRANSITION_DURATION_PROP, @@ -602,6 +768,15 @@ var DETECT_STAGGER_CSS_PROPERTIES = { animationDelay: ANIMATION_DELAY_PROP }; +function getCssKeyframeDurationStyle(duration) { + return [ANIMATION_DURATION_PROP, duration + 's']; +} + +function getCssDelayStyle(delay, isKeyframeAnimation) { + var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; + return [prop, delay + 's']; +} + function computeCssStyles($window, element, properties) { var styles = Object.create(null); var detectedStyles = $window.getComputedStyle(element) || {}; @@ -658,37 +833,6 @@ function getCssTransitionDurationStyle(duration, applyOnlyDuration) { return [style, value]; } -function getCssKeyframeDurationStyle(duration) { - return [ANIMATION_DURATION_PROP, duration + 's']; -} - -function getCssDelayStyle(delay, isKeyframeAnimation) { - var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; - return [prop, delay + 's']; -} - -function blockTransitions(node, duration) { - // we use a negative delay value since it performs blocking - // yet it doesn't kill any existing transitions running on the - // same element which makes this safe for class-based animations - var value = duration ? '-' + duration + 's' : ''; - applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]); - return [TRANSITION_DELAY_PROP, value]; -} - -function blockKeyframeAnimations(node, applyBlock) { - var value = applyBlock ? 'paused' : ''; - var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; - applyInlineStyle(node, [key, value]); - return [key, value]; -} - -function applyInlineStyle(node, styleTuple) { - var prop = styleTuple[0]; - var value = styleTuple[1]; - node.style[prop] = value; -} - function createLocalCacheLookup() { var cache = Object.create(null); return { @@ -716,14 +860,31 @@ function createLocalCacheLookup() { }; } +// we do not reassign an already present style value since +// if we detect the style property value again we may be +// detecting styles that were added via the `from` styles. +// We make use of `isDefined` here since an empty string +// or null value (which is what getPropertyValue will return +// for a non-existing style) will still be marked as a valid +// value for the style (a falsy value implies that the style +// is to be removed at the end of the animation). If we had a simple +// "OR" statement then it would not be enough to catch that. +function registerRestorableStyles(backup, node, properties) { + forEach(properties, function(prop) { + backup[prop] = isDefined(backup[prop]) + ? backup[prop] + : node.style.getPropertyValue(prop); + }); +} + var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var gcsLookup = createLocalCacheLookup(); var gcsStaggerLookup = createLocalCacheLookup(); this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', - '$document', '$sniffer', '$$rAFScheduler', + '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', function($window, $$jqLite, $$AnimateRunner, $timeout, - $document, $sniffer, $$rAFScheduler) { + $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); @@ -780,7 +941,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return stagger || {}; } - var bod = getDomNode($document).body; + var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { rafWaitQueue.push(callback); @@ -788,27 +949,19 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { gcsLookup.flush(); gcsStaggerLookup.flush(); - //the line below will force the browser to perform a repaint so - //that all the animated elements within the animation frame will - //be properly updated and drawn on screen. This is required to - //ensure that the preparation animation is properly flushed so that - //the active state picks up from there. DO NOT REMOVE THIS LINE. - //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH - //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND - //WILL TAKE YEARS AWAY FROM YOUR LIFE. - var width = bod.offsetWidth + 1; + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); // we use a for loop to ensure that if the queue is changed // during this looping then it will consider new requests for (var i = 0; i < rafWaitQueue.length; i++) { - rafWaitQueue[i](width); + rafWaitQueue[i](pageWidth); } rafWaitQueue.length = 0; }); } - return init; - function computeTimings(node, className, cacheKey) { var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); var aD = timings.animationDelay; @@ -823,14 +976,24 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return timings; } - function init(element, options) { + return function init(element, initialOptions) { + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = prepareAnimationOptions(copy(options)); + } + + var restoreStyles = {}; var node = getDomNode(element); - if (!node || !node.parentNode) { + if (!node + || !node.parentNode + || !$$animateQueue.enabled()) { return closeAndReturnNoopAnimator(); } - options = prepareAnimationOptions(options); - var temporaryStyles = []; var classes = element.attr('class'); var styles = packageStyles(options); @@ -843,6 +1006,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var maxDelayTime; var maxDuration; var maxDurationTime; + var startTime; + var events = []; if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { return closeAndReturnNoopAnimator(); @@ -857,20 +1022,20 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var addRemoveClassName = ''; if (isStructural) { - structuralClassName = pendClasses(method, 'ng-', true); + structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); } else if (method) { structuralClassName = method; } if (options.addClass) { - addRemoveClassName += pendClasses(options.addClass, '-add'); + addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); } if (options.removeClass) { if (addRemoveClassName.length) { addRemoveClassName += ' '; } - addRemoveClassName += pendClasses(options.removeClass, '-remove'); + addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); } // there may be a situation where a structural animation is combined together @@ -881,12 +1046,11 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // there actually is a detected transition or keyframe animation if (options.applyClassesEarly && addRemoveClassName.length) { applyAnimationClasses(element, options); - addRemoveClassName = ''; } - var setupClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); - var fullClassName = classes + ' ' + setupClasses; - var activeClasses = pendClasses(setupClasses, '-active'); + var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); + var fullClassName = classes + ' ' + preparationClasses; + var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); var hasToStyles = styles.to && Object.keys(styles.to).length > 0; var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; @@ -895,7 +1059,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // unless there a is raw keyframe value that is applied to the element. if (!containsKeyframeAnimation && !hasToStyles - && !setupClasses) { + && !preparationClasses) { return closeAndReturnNoopAnimator(); } @@ -910,10 +1074,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }; } else { cacheKey = gcsHashFn(node, fullClassName); - stagger = computeCachedCssStaggerStyles(node, setupClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); + stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); } - $$jqLite.addClass(element, setupClasses); + if (!options.$$skipPreparationClasses) { + $$jqLite.addClass(element, preparationClasses); + } var applyOnlyDuration; @@ -952,7 +1118,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { // transition delay to allow for the transition to naturally do it's thing. The beauty here is // that if there is no transition defined then nothing will happen and this will also allow // other transitions to be stacked on top of each other without any chopping them out. - if (isFirst) { + if (isFirst && !options.skipBlocking) { blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); } @@ -994,6 +1160,23 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return closeAndReturnNoopAnimator(); } + if (options.delay != null) { + var delayStyle; + if (typeof options.delay !== "boolean") { + delayStyle = parseFloat(options.delay); + // number in options.delay means we have to recalculate the delay for the closing timeout + maxDelay = Math.max(delayStyle, 0); + } + + if (flags.applyTransitionDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle)); + } + + if (flags.applyAnimationDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle, true)); + } + } + // we need to recalculate the delay value since we used a pre-emptive negative // delay value and the delay value is required for the final event checking. This // property will ensure that this will happen after the RAF phase has passed. @@ -1010,12 +1193,18 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { stagger.animationDuration === 0; } - applyAnimationFromStyles(element, options); - if (!flags.blockTransition) { - blockTransitions(node, false); + if (options.from) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); + } + applyAnimationFromStyles(element, options); } - applyBlocking(maxDuration); + if (flags.blockTransition || flags.blockKeyframeAnimation) { + applyBlocking(maxDuration); + } else if (!options.skipBlocking) { + blockTransitions(node, false); + } // TODO(matsko): for 1.5 change this code to have an animator object for better debugging return { @@ -1058,7 +1247,9 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { animationClosed = true; animationPaused = false; - $$jqLite.removeClass(element, setupClasses); + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); + } $$jqLite.removeClass(element, activeClasses); blockKeyframeAnimations(node, false); @@ -1074,6 +1265,13 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); applyAnimationStyles(element, options); + if (Object.keys(restoreStyles).length) { + forEach(restoreStyles, function(value, prop) { + value ? node.style.setProperty(prop, value) + : node.style.removeProperty(prop); + }); + } + // the reason why we have this option is to allow a synchronous closing callback // that is fired as SOON as the animation ends (when the CSS is removed) or if // the animation never takes off at all. A good example is a leave animation since @@ -1083,6 +1281,18 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { options.onDone(); } + if (events && events.length) { + // Remove the transitionend / animationend listener(s) + element.off(events.join(' '), onAnimationProgress); + } + + //Cancel the fallback closing timeout and remove the timer data + var animationTimerData = element.data(ANIMATE_TIMER_KEY); + if (animationTimerData) { + $timeout.cancel(animationTimerData[0].timer); + element.removeData(ANIMATE_TIMER_KEY); + } + // if the preparation function fails then the promise is not setup if (runner) { runner.complete(!rejected); @@ -1105,6 +1315,8 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { cancel: cancelFn }); + // should flush the cache animation + waitUntilQuiet(noop); close(); return { @@ -1116,6 +1328,33 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { }; } + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + + // we now always use `Date.now()` due to the recent changes with + // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info) + var timeStamp = ev.$manualTimeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animationPauseds sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); + } + } + function start() { if (animationClosed) return; if (!node.parentNode) { @@ -1123,8 +1362,6 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { return; } - var startTime, events = []; - // even though we only pause keyframe animations here the pause flag // will still happen when transitions are used. Only the transition will // not be paused since that is not possible. If the animation ends when @@ -1185,7 +1422,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { $$jqLite.addClass(element, activeClasses); if (flags.recalculateTimingStyles) { - fullClassName = node.className + ' ' + setupClasses; + fullClassName = node.className + ' ' + preparationClasses; cacheKey = gcsHashFn(node, fullClassName); timings = computeTimings(node, fullClassName, cacheKey); @@ -1202,27 +1439,16 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { flags.hasAnimations = timings.animationDuration > 0; } - if (flags.applyTransitionDelay || flags.applyAnimationDelay) { + if (flags.applyAnimationDelay) { relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) ? parseFloat(options.delay) : relativeDelay; maxDelay = Math.max(relativeDelay, 0); - - var delayStyle; - if (flags.applyTransitionDelay) { - timings.transitionDelay = relativeDelay; - delayStyle = getCssDelayStyle(relativeDelay); - temporaryStyles.push(delayStyle); - node.style[delayStyle[0]] = delayStyle[1]; - } - - if (flags.applyAnimationDelay) { - timings.animationDelay = relativeDelay; - delayStyle = getCssDelayStyle(relativeDelay, true); - temporaryStyles.push(delayStyle); - node.style[delayStyle[0]] = delayStyle[1]; - } + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; } maxDelayTime = maxDelay * ONE_SECOND; @@ -1251,44 +1477,58 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { } startTime = Date.now(); - element.on(events.join(' '), onAnimationProgress); - $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); + } + } - applyAnimationToStyles(element, options); - } + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } - function onAnimationExpired() { - // although an expired animation is a failed animation, getting to - // this outcome is very easy if the CSS code screws up. Therefore we - // should still continue normally as if the animation completed correctly. - close(); + if (events.length) { + element.on(events.join(' '), onAnimationProgress); + } + + if (options.to) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); + } + applyAnimationToStyles(element, options); + } } - function onAnimationProgress(event) { - event.stopPropagation(); - var ev = event.originalEvent || event; - var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); - - /* Firefox (or possibly just Gecko) likes to not round values up - * when a ms measurement is used for the animation */ - var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); - - /* $manualTimeStamp is a mocked timeStamp value which is set - * within browserTrigger(). This is only here so that tests can - * mock animations properly. Real events fallback to event.timeStamp, - * or, if they don't, then a timeStamp is automatically created for them. - * We're checking to see if the timeStamp surpasses the expected delay, - * but we're using elapsedTime instead of the timeStamp on the 2nd - * pre-condition since animations sometimes close off early */ - if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { - // we set this flag to ensure that if the transition is paused then, when resumed, - // the animation will automatically close itself since transitions cannot be paused. - animationCompleted = true; - close(); + function onAnimationExpired() { + var animationsData = element.data(ANIMATE_TIMER_KEY); + + // this will be false in the event that the element was + // removed from the DOM (via a leave animation or something + // similar) + if (animationsData) { + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); + } + element.removeData(ANIMATE_TIMER_KEY); } } } - } + }; }]; }]; @@ -1301,16 +1541,27 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; - this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$document', '$sniffer', - function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $document, $sniffer) { + function isDocumentFragment(node) { + return node.parentNode && node.parentNode.nodeType === 11; + } + + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { // only browsers that support these properties can render animations if (!$sniffer.animations && !$sniffer.transitions) return noop; - var bodyNode = getDomNode($document).body; + var bodyNode = $document[0].body; var rootNode = getDomNode($rootElement); - var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode); + var rootBodyElement = jqLite( + // this is to avoid using something that exists outside of the body + // we also special case the doc fragement case because our unit test code + // appends the $rootElement to the body after the app has been bootstrapped + isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode + ); + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); return function initDriverFn(animationDetails) { return animationDetails.from && animationDetails.to @@ -1462,8 +1713,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro } function prepareFromToAnchorAnimation(from, to, classes, anchors) { - var fromAnimation = prepareRegularAnimation(from); - var toAnimation = prepareRegularAnimation(to); + var fromAnimation = prepareRegularAnimation(from, noop); + var toAnimation = prepareRegularAnimation(to, noop); var anchorAnimations = []; forEach(anchors, function(anchor) { @@ -1519,19 +1770,23 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro var options = animationDetails.options || {}; if (animationDetails.structural) { - // structural animations ensure that the CSS classes are always applied - // before the detection starts. - options.structural = options.applyClassesEarly = true; + options.event = animationDetails.event; + options.structural = true; + options.applyClassesEarly = true; // we special case the leave animation since we want to ensure that // the element is removed as soon as the animation is over. Otherwise // a flicker might appear or the element may not be removed at all - options.event = animationDetails.event; - if (options.event === 'leave') { + if (animationDetails.event === 'leave') { options.onDone = options.domOperation; } - } else { - options.event = null; + } + + // We assign the preparationClasses as the actual animation event since + // the internals of $animateCss will just suffix the event token values + // with `-active` to trigger the animation. + if (options.preparationClasses) { + options.event = concatWithSpace(options.event, options.preparationClasses); } var animator = $animateCss(element, options); @@ -1550,12 +1805,14 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro // by the time... var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { - this.$get = ['$injector', '$$AnimateRunner', '$$rAFMutex', '$$jqLite', - function($injector, $$AnimateRunner, $$rAFMutex, $$jqLite) { + this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', + function($injector, $$AnimateRunner, $$jqLite) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); // $animateJs(element, 'enter'); return function(element, event, classes, options) { + var animationClosed = false; + // the `classes` argument is optional and if it is not used // then the classes will be resolved from the element's className // property as well as options.addClass/options.removeClass. @@ -1608,8 +1865,32 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); } + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } + + var runner; + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); + } else { + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); var closeActiveAnimations; var chain = []; @@ -1634,8 +1915,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { }); } - var animationClosed = false; - var runner = new $$AnimateRunner({ + runner.setHost({ end: function() { endAnimations(); }, @@ -1648,9 +1928,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { return runner; function onComplete(success) { - animationClosed = true; - applyOptions(); - applyAnimationStyles(element, options); + close(success); runner.complete(success); } @@ -1870,6 +2148,7 @@ var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; + var ONE_SPACE = ' '; var rules = this.rules = { skip: [], @@ -1877,28 +2156,50 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { join: [] }; + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { return rules[ruleType].some(function(fn) { return fn(element, currentAnimation, previousAnimation); }); } - function hasAnimationClasses(options, and) { - options = options || {}; - var a = (options.addClass || '').length > 0; - var b = (options.removeClass || '').length > 0; + function hasAnimationClasses(animation, and) { + var a = (animation.addClass || '').length > 0; + var b = (animation.removeClass || '').length > 0; return and ? a && b : a || b; } rules.join.push(function(element, newAnimation, currentAnimation) { // if the new animation is class-based then we can just tack that on - return !newAnimation.structural && hasAnimationClasses(newAnimation.options); + return !newAnimation.structural && hasAnimationClasses(newAnimation); }); rules.skip.push(function(element, newAnimation, currentAnimation) { // there is no need to animate anything if no classes are being added and // there is no structural animation that will be triggered - return !newAnimation.structural && !hasAnimationClasses(newAnimation.options); + return !newAnimation.structural && !hasAnimationClasses(newAnimation); }); rules.skip.push(function(element, newAnimation, currentAnimation) { @@ -1908,8 +2209,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); rules.skip.push(function(element, newAnimation, currentAnimation) { - // if there is a current animation then skip the class-based animation - return currentAnimation.structural && !newAnimation.structural; + // if there is an ongoing current animation then don't even bother running the class-based animation + return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural; }); rules.cancel.push(function(element, newAnimation, currentAnimation) { @@ -1924,23 +2225,46 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }); rules.cancel.push(function(element, newAnimation, currentAnimation) { - var nO = newAnimation.options; - var cO = currentAnimation.options; + var nA = newAnimation.addClass; + var nR = newAnimation.removeClass; + var cA = currentAnimation.addClass; + var cR = currentAnimation.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } - // if the exact same CSS class is added/removed then it's safe to cancel it - return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass); + return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); }); this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', - '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', function($$rAF, $rootScope, $rootElement, $document, $$HashMap, - $$animation, $$AnimateRunner, $templateRequest, $$jqLite) { + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { var activeAnimationsLookup = new $$HashMap(); var disabledElementsLookup = new $$HashMap(); - var animationsEnabled = null; + function postDigestTaskFactory() { + var postDigestCalled = false; + return function(fn) { + // we only issue a call to postDigest before + // it has first passed. This prevents any callbacks + // from not firing once the animation has completed + // since it will be out of the digest cycle. + if (postDigestCalled) { + fn(); + } else { + $rootScope.$$postDigest(function() { + postDigestCalled = true; + fn(); + }); + } + }; + } + // Wait until all directive and route-related templates are downloaded and // compiled. The $templateRequest.totalPendingRequests variable keeps track of // all of the remote templates being currently downloaded. If there are no @@ -1970,8 +2294,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } ); - var bodyElement = jqLite($document[0].body); - var callbackRegistry = {}; // remember that the classNameFilter is set during the provider/config @@ -1985,18 +2307,28 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - function normalizeAnimationOptions(element, options) { - return mergeAnimationOptions(element, options, {}); + function normalizeAnimationDetails(element, animation) { + return mergeAnimationDetails(element, animation, {}); } - function findCallbacks(element, event) { + // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var contains = Node.prototype.contains || function(arg) { + // jshint bitwise: false + return this === arg || !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true + }; + + function findCallbacks(parent, element, event) { var targetNode = getDomNode(element); + var targetParentNode = getDomNode(parent); var matches = []; var entries = callbackRegistry[event]; if (entries) { forEach(entries, function(entry) { - if (entry.node.contains(targetNode)) { + if (contains.call(entry.node, targetNode)) { + matches.push(entry.callback); + } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { matches.push(entry.callback); } }); @@ -2005,14 +2337,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return matches; } - function triggerCallback(event, element, phase, data) { - $$rAF(function() { - forEach(findCallbacks(element, event), function(callback) { - callback(element, phase, data); - }); - }); - } - return { on: function(event, container, callback) { var node = extractElementNode(container); @@ -2079,12 +2403,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { bool = !recordExists; } else { // (element, bool) - Element setter - bool = !!bool; - if (!bool) { - disabledElementsLookup.put(node, true); - } else if (recordExists) { - disabledElementsLookup.remove(node); - } + disabledElementsLookup.put(node, !bool); } } } @@ -2093,7 +2412,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { } }; - function queueAnimation(element, event, options) { + function queueAnimation(element, event, initialOptions) { + // we always make a copy of the options since + // there should never be any side effects on + // the input data when running `$animateCss`. + var options = copy(initialOptions); + var node, parent; element = stripCommentsFromElement(element); if (element) { @@ -2107,22 +2431,25 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // These methods will become available after the digest has passed var runner = new $$AnimateRunner(); - // there are situations where a directive issues an animation for - // a jqLite wrapper that contains only comment nodes... If this - // happens then there is no way we can perform an animation - if (!node) { - close(); - return runner; - } + // this is used to trigger callbacks in postDigest mode + var runInNextPostDigestOrNow = postDigestTaskFactory(); if (isArray(options.addClass)) { options.addClass = options.addClass.join(' '); } + if (options.addClass && !isString(options.addClass)) { + options.addClass = null; + } + if (isArray(options.removeClass)) { options.removeClass = options.removeClass.join(' '); } + if (options.removeClass && !isString(options.removeClass)) { + options.removeClass = null; + } + if (options.from && !isObject(options.from)) { options.from = null; } @@ -2131,6 +2458,14 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { options.to = null; } + // there are situations where a directive issues an animation for + // a jqLite wrapper that contains only comment nodes... If this + // happens then there is no way we can perform an animation + if (!node) { + close(); + return runner; + } + var className = [node.className, options.addClass, options.removeClass].join(' '); if (!isAnimatableClassName(className)) { close(); @@ -2142,7 +2477,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // this is a hard disable of all animations for the application or on // the element itself, therefore there is no need to continue further // past this point if not enabled - var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node); + // Animations are also disabled if the document is currently hidden (page is not visible + // to the user), because browsers slow down or do not flush calls to requestAnimationFrame + var skipAnimations = !animationsEnabled || $document[0].hidden || disabledElementsLookup.get(node); var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; var hasExistingAnimation = !!existingAnimation.state; @@ -2165,6 +2502,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { structural: isStructural, element: element, event: event, + addClass: options.addClass, + removeClass: options.removeClass, close: close, options: options, runner: runner @@ -2177,11 +2516,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { close(); return runner; } else { - mergeAnimationOptions(element, existingAnimation.options, options); + mergeAnimationDetails(element, existingAnimation, newAnimation); return existingAnimation.runner; } } - var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); if (cancelAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { @@ -2195,8 +2533,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // method which will call the runner methods in async. existingAnimation.close(); } else { - // this will merge the existing animation options into this new follow-up animation - mergeAnimationOptions(element, newAnimation.options, existingAnimation.options); + // this will merge the new animation options into existing animation options + mergeAnimationDetails(element, existingAnimation, newAnimation); + + return existingAnimation.runner; } } else { // a joined animation means that this animation will take over the existing one @@ -2205,18 +2545,23 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); if (joinAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { - normalizeAnimationOptions(element, options); + normalizeAnimationDetails(element, newAnimation); } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + event = newAnimation.event = existingAnimation.event; - options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options); - return runner; + options = mergeAnimationDetails(element, existingAnimation, newAnimation); + + //we return the same runner since only the option values of this animation will + //be fed into the `existingAnimation`. + return existingAnimation.runner; } } } } else { // normalization in this case means that it removes redundant CSS classes that // already exist (addClass) or do not exist (removeClass) on the element - normalizeAnimationOptions(element, options); + normalizeAnimationDetails(element, newAnimation); } // when the options are merged and cleaned up we may end up not having to do @@ -2226,7 +2571,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { if (!isValidAnimation) { // animate (from/to) can be quickly checked first, otherwise we check if any classes are present isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) - || hasAnimationClasses(newAnimation.options); + || hasAnimationClasses(newAnimation); } if (!isValidAnimation) { @@ -2235,10 +2580,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return runner; } - if (isStructural) { - closeParentClassBasedAnimations(parent); - } - // the counter keeps track of cancelled animations var counter = (existingAnimation.counter || 0) + 1; newAnimation.counter = counter; @@ -2260,7 +2601,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { var isValidAnimation = parentElement.length > 0 && (animationDetails.event === 'animate' || animationDetails.structural - || hasAnimationClasses(animationDetails.options)); + || hasAnimationClasses(animationDetails)); // this means that the previous animation was cancelled // even if the follow-up animation is the same event @@ -2292,16 +2633,13 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // this combined multiple class to addClass / removeClass into a setClass event // so long as a structural event did not take over the animation - event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true) + event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) ? 'setClass' : animationDetails.event; - if (animationDetails.structural) { - closeParentClassBasedAnimations(parentElement); - } - markElementAnimationState(element, RUNNING_STATE); var realRunner = $$animation(element, event, animationDetails.options); + realRunner.done(function(status) { close(!status); var animationDetails = activeAnimationsLookup.get(node); @@ -2320,11 +2658,25 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return runner; function notifyProgress(runner, event, phase, data) { - triggerCallback(event, element, phase, data); + runInNextPostDigestOrNow(function() { + var callbacks = findCallbacks(parent, element, event); + if (callbacks.length) { + // do not optimize this call here to RAF because + // we don't know how heavy the callback code here will + // be and if this code is buffered then this can + // lead to a performance regression. + $$rAF(function() { + forEach(callbacks, function(callback) { + callback(element, phase, data); + }); + }); + } + }); runner.progress(event, phase, data); } function close(reject) { // jshint ignore:line + clearGeneratedClasses(element, options); applyAnimationClasses(element, options); applyAnimationStyles(element, options); options.domOperation(); @@ -2338,15 +2690,15 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { forEach(children, function(child) { var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); var animationDetails = activeAnimationsLookup.get(child); - switch (state) { - case RUNNING_STATE: - animationDetails.runner.end(); - /* falls through */ - case PRE_DIGEST_STATE: - if (animationDetails) { + if (animationDetails) { + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: activeAnimationsLookup.remove(child); - } - break; + break; + } } }); } @@ -2361,67 +2713,61 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); } - function closeParentClassBasedAnimations(startingElement) { - var parentNode = getDomNode(startingElement); - do { - if (!parentNode || parentNode.nodeType !== ELEMENT_NODE) break; - - var animationDetails = activeAnimationsLookup.get(parentNode); - if (animationDetails) { - examineParentAnimation(parentNode, animationDetails); - } - - parentNode = parentNode.parentNode; - } while (true); - - // since animations are detected from CSS classes, we need to flush all parent - // class-based animations so that the parent classes are all present for child - // animations to properly function (otherwise any CSS selectors may not work) - function examineParentAnimation(node, animationDetails) { - // enter/leave/move always have priority - if (animationDetails.structural || !hasAnimationClasses(animationDetails.options)) return; - - if (animationDetails.state === RUNNING_STATE) { - animationDetails.runner.end(); - } - clearElementAnimationState(node); - } - } - + /** + * This fn returns false if any of the following is true: + * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed + * b) a parent element has an ongoing structural animation, and animateChildren is false + * c) the element is not a child of the body + * d) the element is not a child of the $rootElement + */ function areAnimationsAllowed(element, parentElement, event) { - var bodyElementDetected = false; - var rootElementDetected = false; + var bodyElement = jqLite($document[0].body); + var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); var parentAnimationDetected = false; var animateChildren; + var elementDisabled = disabledElementsLookup.get(getDomNode(element)); - var parentHost = element.data(NG_ANIMATE_PIN_DATA); + var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); if (parentHost) { parentElement = parentHost; } - while (parentElement && parentElement.length) { + parentElement = getDomNode(parentElement); + + while (parentElement) { if (!rootElementDetected) { // angular doesn't want to attempt to animate elements outside of the application // therefore we need to ensure that the rootElement is an ancestor of the current element rootElementDetected = isMatchingElement(parentElement, $rootElement); } - var parentNode = parentElement[0]; - if (parentNode.nodeType !== ELEMENT_NODE) { + if (parentElement.nodeType !== ELEMENT_NODE) { // no point in inspecting the #document element break; } - var details = activeAnimationsLookup.get(parentNode) || {}; + var details = activeAnimationsLookup.get(parentElement) || {}; // either an enter, leave or move animation will commence // therefore we can't allow any animations to take place // but if a parent animation is class-based then that's ok if (!parentAnimationDetected) { - parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode); + var parentElementDisabled = disabledElementsLookup.get(parentElement); + + if (parentElementDisabled === true && elementDisabled !== false) { + // disable animations if the user hasn't explicitly enabled animations on the + // current element + elementDisabled = true; + // element is disabled via parent element, no need to check anything else + break; + } else if (parentElementDisabled === false) { + elementDisabled = false; + } + parentAnimationDetected = details.structural; } if (isUndefined(animateChildren) || animateChildren === true) { - var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA); + var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); if (isDefined(value)) { animateChildren = value; } @@ -2430,28 +2776,32 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { // there is no need to continue traversing at this point if (parentAnimationDetected && animateChildren === false) break; - if (!rootElementDetected) { - // angular doesn't want to attempt to animate elements outside of the application - // therefore we need to ensure that the rootElement is an ancestor of the current element - rootElementDetected = isMatchingElement(parentElement, $rootElement); - if (!rootElementDetected) { - parentHost = parentElement.data(NG_ANIMATE_PIN_DATA); - if (parentHost) { - parentElement = parentHost; - } - } - } - if (!bodyElementDetected) { - // we also need to ensure that the element is or will be apart of the body element + // we also need to ensure that the element is or will be a part of the body element // otherwise it is pointless to even issue an animation to be rendered bodyElementDetected = isMatchingElement(parentElement, bodyElement); } - parentElement = parentElement.parent(); + if (bodyElementDetected && rootElementDetected) { + // If both body and root have been found, any other checks are pointless, + // as no animation data should live outside the application + break; + } + + if (!rootElementDetected) { + // If no rootElement is detected, check if the parentElement is pinned to another element + parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA); + if (parentHost) { + // The pin target element becomes the next parent element + parentElement = getDomNode(parentHost); + continue; + } + } + + parentElement = parentElement.parentNode; } - var allowAnimation = !parentAnimationDetected || animateChildren; + var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; return allowAnimation && rootElementDetected && bodyElementDetected; } @@ -2471,184 +2821,112 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { }]; }]; -var $$rAFMutexFactory = ['$$rAF', function($$rAF) { - return function() { - var passed = false; - $$rAF(function() { - passed = true; - }); - return function(fn) { - passed ? fn() : $$rAF(fn); - }; - }; -}]; - -var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) { - var INITIAL_STATE = 0; - var DONE_PENDING_STATE = 1; - var DONE_COMPLETE_STATE = 2; - - AnimateRunner.chain = function(chain, callback) { - var index = 0; - - next(); - function next() { - if (index === chain.length) { - callback(true); - return; - } - - chain[index](function(response) { - if (response === false) { - callback(false); - return; - } - index++; - next(); - }); - } - }; +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; - AnimateRunner.all = function(runners, callback) { - var count = 0; - var status = true; - forEach(runners, function(runner) { - runner.done(onProgress); - }); + var drivers = this.drivers = []; - function onProgress(response) { - status = status && response; - if (++count === runners.length) { - callback(status); - } - } - }; + var RUNNER_STORAGE_KEY = '$$animationRunner'; - function AnimateRunner(host) { - this.setHost(host); + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } - this._doneCallbacks = []; - this._runInAnimationFrame = $$rAFMutex(); - this._state = 0; + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); } - AnimateRunner.prototype = { - setHost: function(host) { - this.host = host || {}; - }, + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } - done: function(fn) { - if (this._state === DONE_COMPLETE_STATE) { - fn(); - } else { - this._doneCallbacks.push(fn); - } - }, + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { - progress: noop, + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - getPromise: function() { - if (!this.promise) { - var self = this; - this.promise = $q(function(resolve, reject) { - self.done(function(status) { - status === false ? reject() : resolve(); - }); + function sortAnimations(animations) { + var tree = { children: [] }; + var i, lookup = new $$HashMap(); + + // this is done first beforehand so that the hashmap + // is filled with a list of the elements that will be animated + for (i = 0; i < animations.length; i++) { + var animation = animations[i]; + lookup.put(animation.domNode, animations[i] = { + domNode: animation.domNode, + fn: animation.fn, + children: [] }); } - return this.promise; - }, - - then: function(resolveHandler, rejectHandler) { - return this.getPromise().then(resolveHandler, rejectHandler); - }, - 'catch': function(handler) { - return this.getPromise()['catch'](handler); - }, - - 'finally': function(handler) { - return this.getPromise()['finally'](handler); - }, - - pause: function() { - if (this.host.pause) { - this.host.pause(); + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); } - }, - resume: function() { - if (this.host.resume) { - this.host.resume(); - } - }, + return flatten(tree); - end: function() { - if (this.host.end) { - this.host.end(); - } - this._resolve(true); - }, + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; - cancel: function() { - if (this.host.cancel) { - this.host.cancel(); - } - this._resolve(false); - }, + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); - complete: function(response) { - var self = this; - if (self._state === INITIAL_STATE) { - self._state = DONE_PENDING_STATE; - self._runInAnimationFrame(function() { - self._resolve(response); - }); - } - }, + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; + } + parentNode = parentNode.parentNode; + } - _resolve: function(response) { - if (this._state !== DONE_COMPLETE_STATE) { - forEach(this._doneCallbacks, function(fn) { - fn(response); - }); - this._doneCallbacks.length = 0; - this._state = DONE_COMPLETE_STATE; + (parentEntry || tree).children.push(entry); + return entry; } - } - }; - - return AnimateRunner; -}]; - -var $$AnimationProvider = ['$animateProvider', function($animateProvider) { - var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; - - var drivers = this.drivers = []; - - var RUNNER_STORAGE_KEY = '$$animationRunner'; - function setRunner(element, runner) { - element.data(RUNNER_STORAGE_KEY, runner); - } + function flatten(tree) { + var result = []; + var queue = []; + var i; - function removeRunner(element) { - element.removeData(RUNNER_STORAGE_KEY); - } - - function getRunner(element) { - return element.data(RUNNER_STORAGE_KEY); - } + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } - this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$rAFScheduler', - function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$rAFScheduler) { + var remainingLevelEntries = queue.length; + var nextLevelEntries = 0; + var row = []; + + for (i = 0; i < queue.length; i++) { + var entry = queue[i]; + if (remainingLevelEntries <= 0) { + remainingLevelEntries = nextLevelEntries; + nextLevelEntries = 0; + result.push(row); + row = []; + } + row.push(entry.fn); + entry.children.forEach(function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } - var animationQueue = []; - var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + if (row.length) { + result.push(row); + } - var totalPendingClassBasedAnimations = 0; - var totalActiveClassBasedAnimations = 0; - var classBasedAnimationsQueue = []; + return result; + } + } // TODO(matsko): document the signature in a better way return function(element, event, options) { @@ -2678,10 +2956,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { options.tempClasses = null; } - var classBasedIndex; - if (!isStructural) { - classBasedIndex = totalPendingClassBasedAnimations; - totalPendingClassBasedAnimations += 1; + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); } animationQueue.push({ @@ -2690,7 +2968,6 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { element: element, classes: classes, event: event, - classBasedIndex: classBasedIndex, structural: isStructural, options: options, beforeStart: beforeStart, @@ -2705,10 +2982,6 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { if (animationQueue.length > 1) return runner; $rootScope.$$postDigest(function() { - totalActiveClassBasedAnimations = totalPendingClassBasedAnimations; - totalPendingClassBasedAnimations = 0; - classBasedAnimationsQueue.length = 0; - var animations = []; forEach(animationQueue, function(entry) { // the element was destroyed early on which removed the runner @@ -2716,67 +2989,58 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { // at all and it already has been closed due to destruction. if (getRunner(entry.element)) { animations.push(entry); + } else { + entry.close(); } }); // now any future animations will be in another postDigest animationQueue.length = 0; - forEach(groupAnimations(animations), function(animationEntry) { - if (animationEntry.structural) { - triggerAnimationStart(); - } else { - classBasedAnimationsQueue.push({ - node: getDomNode(animationEntry.element), - fn: triggerAnimationStart - }); - - if (animationEntry.classBasedIndex === totalActiveClassBasedAnimations - 1) { - // we need to sort each of the animations in order of parent to child - // relationships. This ensures that the child classes are applied at the - // right time. - classBasedAnimationsQueue = classBasedAnimationsQueue.sort(function(a,b) { - return b.node.contains(a.node); - }).map(function(entry) { - return entry.fn; - }); - - $$rAFScheduler(classBasedAnimationsQueue); - } - } - - function triggerAnimationStart() { - // it's important that we apply the `ng-animate` CSS class and the - // temporary classes before we do any driver invoking since these - // CSS classes may be required for proper CSS detection. - animationEntry.beforeStart(); - - var startAnimationFn, closeFn = animationEntry.close; - - // in the event that the element was removed before the digest runs or - // during the RAF sequencing then we should not trigger the animation. - var targetElement = animationEntry.anchors - ? (animationEntry.from.element || animationEntry.to.element) - : animationEntry.element; - - if (getRunner(targetElement) && getDomNode(targetElement).parentNode) { - var operation = invokeFirstDriver(animationEntry); - if (operation) { - startAnimationFn = operation.start; + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; + + forEach(groupedAnimations, function(animationEntry) { + toBeSortedAnimations.push({ + domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + fn: function triggerAnimationStart() { + // it's important that we apply the `ng-animate` CSS class and the + // temporary classes before we do any driver invoking since these + // CSS classes may be required for proper CSS detection. + animationEntry.beforeStart(); + + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that the element was removed before the digest runs or + // during the RAF sequencing then we should not trigger the animation. + var targetElement = animationEntry.anchors + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; + } } - } - if (!startAnimationFn) { - closeFn(); - } else { - var animationRunner = startAnimationFn(); - animationRunner.done(function(status) { - closeFn(!status); - }); - updateAnimationRunners(animationEntry, animationRunner); + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } } - } + }); }); + + // we need to sort each of the animations in order of parent to child + // relationships. This ensures that the child classes are applied at the + // right time. + $$rAFScheduler(sortAnimations(toBeSortedAnimations)); }); return runner; @@ -2920,6 +3184,10 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { if (tempClasses) { $$jqLite.addClass(element, tempClasses); } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } } function updateAnimationRunners(animation, newRunner) { @@ -2963,10 +3231,9 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { /* global angularAnimateModule: true, - $$rAFMutexFactory, + $$AnimateAsyncRunFactory, $$rAFSchedulerFactory, $$AnimateChildrenDirective, - $$AnimateRunnerFactory, $$AnimateQueueProvider, $$AnimationProvider, $AnimateCssProvider, @@ -2981,7 +3248,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * @description * * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via - * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` then the animation hooks are enabled for an Angular app. + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app. * * <div doc-module-components="ngAnimate"></div> * @@ -3014,7 +3281,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation. * - * The example below shows how an `enter` animation can be made possible on a element using `ng-if`: + * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: * * ```html * <div ng-if="bool" class="fade"> @@ -3149,8 +3416,8 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * /* this will have a 100ms delay between each successive leave animation */ * transition-delay: 0.1s; * - * /* in case the stagger doesn't work then the duration value - * must be set to 0 to avoid an accidental CSS inheritance */ + * /* As of 1.4.4, this must always be set: it signals ngAnimate + * to not accidentally inherit a delay property from another CSS class */ * transition-duration: 0s; * } * .my-animation.ng-enter.ng-enter-active { @@ -3215,6 +3482,34 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * the CSS class once an animation has completed.) * * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + * <div ng-class="{red: myProp}"> + * <div ng-class="{blue: myProp}"> + * <div class="message" ng-if="myProp"></div> + * </div> + * </div> + * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * * ## JavaScript-based Animations * * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared @@ -3251,7 +3546,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * jQuery(element).fadeOut(1000, doneFn); * } * } - * }] + * }]); * ``` * * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as @@ -3282,7 +3577,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * // do some cool animation and call the doneFn * } * } - * }] + * }]); * ``` * * ## CSS + JS Animations Together @@ -3304,7 +3599,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * jQuery(element).slideIn(1000, doneFn); * } * } - * }] + * }]); * ``` * * ```css @@ -3324,16 +3619,15 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * ```js * myModule.animation('.slide', ['$animateCss', function($animateCss) { * return { - * enter: function(element, doneFn) { + * enter: function(element) { * // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. - * var runner = $animateCss(element, { + * return $animateCss(element, { * event: 'enter', * structural: true - * }).start(); -* runner.done(doneFn); + * }); * } * } - * }] + * }]); * ``` * * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. @@ -3345,18 +3639,17 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * ```js * myModule.animation('.slide', ['$animateCss', function($animateCss) { * return { - * enter: function(element, doneFn) { - * var runner = $animateCss(element, { + * enter: function(element) { + * return $animateCss(element, { * event: 'enter', + * structural: true, * addClass: 'maroon-setting', * from: { height:0 }, * to: { height: 200 } - * }).start(); - * - * runner.done(doneFn); + * }); * } * } - * }] + * }]); * ``` * * Now we can fill in the rest via our transition CSS code: @@ -3698,16 +3991,12 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) { * @description * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. * - * Click here {@link ng.$animate $animate to learn more about animations with `$animate`}. + * Click here {@link ng.$animate to learn more about animations with `$animate`}. */ angular.module('ngAnimate', []) .directive('ngAnimateChildren', $$AnimateChildrenDirective) - - .factory('$$rAFMutex', $$rAFMutexFactory) .factory('$$rAFScheduler', $$rAFSchedulerFactory) - .factory('$$AnimateRunner', $$AnimateRunnerFactory) - .provider('$$animateQueue', $$AnimateQueueProvider) .provider('$$animation', $$AnimationProvider) |