vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.4.3 vs vendor/assets/javascripts/angular-animate.js in angularjs-rails-1.4.4

- old
+ new

@@ -1,7 +1,7 @@ /** - * @license AngularJS v1.4.3 + * @license AngularJS v1.4.4 * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -19,16 +19,64 @@ 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 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 (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 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; -} +}; function assertArg(arg, name, reason) { if (!arg) { throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); } @@ -175,12 +223,25 @@ function mergeAnimationOptions(element, target, newOptions) { 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 { target.addClass = null; } @@ -254,68 +315,72 @@ function getDomNode(element) { return (element instanceof angular.element) ? element[0] : element; } -var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { - var tickQueue = []; - var cancelFn; +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 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)); - nextTick(); +function clearGeneratedClasses(element, options) { + if (options.preparationClasses) { + element.removeClass(options.preparationClasses); + options.preparationClasses = null; } + if (options.activeClasses) { + element.removeClass(options.activeClasses); + options.activeClasses = null; + } +} - /* 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. - * - * The motivation here is that animation code can request more time from the scheduler - * before the next wave runs. This allows for certain DOM properties such as classes to - * be resolved in time for the next animation to run. - */ - scheduler.waitUntilQuiet = function(fn) { - if (cancelFn) cancelFn(); +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]; +} - cancelFn = $$rAF(function() { - cancelFn = null; - fn(); - nextTick(); - }); - }; +function blockKeyframeAnimations(node, applyBlock) { + var value = applyBlock ? 'paused' : ''; + var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; + applyInlineStyle(node, [key, value]); + return [key, value]; +} - return scheduler; +function applyInlineStyle(node, styleTuple) { + var prop = styleTuple[0]; + var value = styleTuple[1]; + node.style[prop] = value; +} - function nextTick() { - if (!tickQueue.length) return; +function concatWithSpace(a,b) { + if (!a) return b; + if (!b) return a; + return a + ' ' + b; +} - var updatedQueue = []; - for (var i = 0; i < tickQueue.length; i++) { - var innerQueue = tickQueue[i]; - runNextTask(innerQueue); - if (innerQueue.length) { - updatedQueue.push(innerQueue); - } - } - tickQueue = updatedQueue; +function $$BodyProvider() { + this.$get = ['$document', function($document) { + return jqLite($document[0].body); + }]; +} - if (!cancelFn) { - $$rAF(function() { - if (!cancelFn) nextTick(); - }); - } - } - - 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); @@ -526,68 +591,24 @@ * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same * CSS delay value. * * `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.) * * @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. */ +var ONE_SECOND = 1000; +var BASE_TEN = 10; -// 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 DETECT_CSS_PROPERTIES = { transitionDuration: TRANSITION_DURATION_PROP, transitionDelay: TRANSITION_DELAY_PROP, transitionProperty: TRANSITION_PROP + PROPERTY_KEY, animationDuration: ANIMATION_DURATION_PROP, @@ -600,10 +621,19 @@ transitionDelay: TRANSITION_DELAY_PROP, animationDuration: ANIMATION_DURATION_PROP, 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) || {}; forEach(properties, function(formalStyleName, actualStyleName) { var val = detectedStyles[formalStyleName]; @@ -656,41 +686,10 @@ value += ' linear all'; } 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 { flush: function() { cache = Object.create(null); @@ -718,14 +717,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var gcsLookup = createLocalCacheLookup(); var gcsStaggerLookup = createLocalCacheLookup(); - this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', - '$document', '$sniffer', '$$rAFScheduler', - function($window, $$jqLite, $$AnimateRunner, $timeout, - $document, $sniffer, $$rAFScheduler) { + this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$forceReflow', '$sniffer', '$$rAF', + function($window, $$jqLite, $$AnimateRunner, $timeout, $$forceReflow, $sniffer, $$rAF) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); var parentCounter = 0; function gcsHashFn(node, extraClasses) { @@ -778,32 +775,30 @@ } return stagger || {}; } - var bod = getDomNode($document).body; + var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { + if (cancelLastRAFRequest) { + cancelLastRAFRequest(); //cancels the request + } rafWaitQueue.push(callback); - $$rAFScheduler.waitUntilQuiet(function() { + cancelLastRAFRequest = $$rAF(function() { + cancelLastRAFRequest = null; 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; }); } @@ -855,24 +850,24 @@ var isStructural = method && options.structural; var structuralClassName = ''; 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 // with CSS classes that need to resolve before the animation is computed. // However this means that there is no explicit CSS code to block the animation @@ -882,22 +877,22 @@ 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; // there is no way we can trigger an animation if no styles and // no classes are being applied which would then trigger a transition, // unless there a is raw keyframe value that is applied to the element. if (!containsKeyframeAnimation && !hasToStyles - && !setupClasses) { + && !preparationClasses) { return closeAndReturnNoopAnimator(); } var cacheKey, stagger; if (options.stagger > 0) { @@ -908,14 +903,16 @@ transitionDuration: 0, animationDuration: 0 }; } 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; if (options.transitionStyle) { var transitionStyle = [TRANSITION_PROP, options.transitionStyle]; @@ -950,11 +947,11 @@ // without causing any combination of transitions to kick in. By adding a negative delay value // it forces the setup class' transition to end immediately. We later then remove the negative // 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); } var timings = computeTimings(node, fullClassName, cacheKey); var relativeDelay = timings.maxDelay; @@ -1009,16 +1006,17 @@ stagger.animationDelay > 0 && stagger.animationDuration === 0; } applyAnimationFromStyles(element, options); - if (!flags.blockTransition) { + + if (flags.blockTransition || flags.blockKeyframeAnimation) { + applyBlocking(maxDuration); + } else if (!options.skipBlocking) { blockTransitions(node, false); } - applyBlocking(maxDuration); - // TODO(matsko): for 1.5 change this code to have an animator object for better debugging return { $$willAnimate: true, end: endFn, start: function() { @@ -1056,11 +1054,13 @@ // the animation again if (animationClosed || (animationCompleted && animationPaused)) return; animationClosed = true; animationPaused = false; - $$jqLite.removeClass(element, setupClasses); + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); + } $$jqLite.removeClass(element, activeClasses); blockKeyframeAnimations(node, false); blockTransitions(node, false); @@ -1183,11 +1183,11 @@ applyAnimationClasses(element, options); $$jqLite.addClass(element, activeClasses); if (flags.recalculateTimingStyles) { - fullClassName = node.className + ' ' + setupClasses; + fullClassName = node.className + ' ' + preparationClasses; cacheKey = gcsHashFn(node, fullClassName); timings = computeTimings(node, fullClassName, cacheKey); relativeDelay = timings.maxDelay; maxDelay = Math.max(relativeDelay, 0); @@ -1250,11 +1250,11 @@ events.push(ANIMATIONEND_EVENT); } startTime = Date.now(); element.on(events.join(' '), onAnimationProgress); - $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime); + $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime, false); applyAnimationToStyles(element, options); } function onAnimationExpired() { @@ -1299,28 +1299,30 @@ var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; 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) { + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer', '$$jqLite', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer, $$jqLite) { // only browsers that support these properties can render animations if (!$sniffer.animations && !$sniffer.transitions) return noop; - var bodyNode = getDomNode($document).body; + var bodyNode = getDomNode($$body); var rootNode = getDomNode($rootElement); var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode); - return function initDriverFn(animationDetails) { + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + return function initDriverFn(animationDetails, onBeforeClassesAppliedCb) { return animationDetails.from && animationDetails.to ? prepareFromToAnchorAnimation(animationDetails.from, animationDetails.to, animationDetails.classes, animationDetails.anchors) - : prepareRegularAnimation(animationDetails); + : prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb); }; function filterCssClasses(classes) { //remove all the `ng-` stuff return classes.replace(/\bng-\S+\b/g, ''); @@ -1460,12 +1462,12 @@ inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); } } 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) { var outElement = anchor['out']; var inElement = anchor['in']; @@ -1512,30 +1514,46 @@ } } }; } - function prepareRegularAnimation(animationDetails) { + function prepareRegularAnimation(animationDetails, onBeforeClassesAppliedCb) { var element = animationDetails.element; var options = animationDetails.options || {}; + // since the ng-EVENT, class-ADD and class-REMOVE classes are applied inside + // of the animateQueue pre and postDigest stages then there is no need to add + // then them here as well. + options.$$skipPreparationClasses = true; + + // during the pre/post digest stages inside of animateQueue we also performed + // the blocking (transition:-9999s) so there is no point in doing that again. + options.skipBlocking = true; + 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; // 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 apply the classes right away since the pre-digest took care of the + // preparation classes. + onBeforeClassesAppliedCb(element); + applyAnimationClasses(element, options); + + // 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); // the driver lookup code inside of $$animation attempts to spawn a // driver one by one until a driver returns a.$$willAnimate animator object. // $animateCss will always return an object, however, it will pass in @@ -1906,12 +1924,12 @@ // be removed from the DOM anyway? return currentAnimation.event == 'leave' && newAnimation.structural; }); 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) { // there can never be two structural animations running at the same time return currentAnimation.structural && newAnimation.structural; @@ -1929,18 +1947,17 @@ // 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); }); - this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', - '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', - function($$rAF, $rootScope, $rootElement, $document, $$HashMap, - $$animation, $$AnimateRunner, $templateRequest, $$jqLite) { + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$body', '$$HashMap', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + function($$rAF, $rootScope, $rootElement, $document, $$body, $$HashMap, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { var activeAnimationsLookup = new $$HashMap(); var disabledElementsLookup = new $$HashMap(); - var animationsEnabled = null; // 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 @@ -1968,12 +1985,10 @@ }); }); } ); - var bodyElement = jqLite($document[0].body); - var callbackRegistry = {}; // remember that the classNameFilter is set during the provider/config // stage therefore we can optimize here and setup a helper function var classNameFilter = $animateProvider.classNameFilter(); @@ -2105,34 +2120,42 @@ // we create a fake runner with a working promise. // 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; - } - 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; } if (options.to && !isObject(options.to)) { 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(); return runner; } @@ -2193,25 +2216,31 @@ // this means that the animation is queued into a digest, but // hasn't started yet. Therefore it is safe to run the close // 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 + mergeAnimationOptions(element, existingAnimation.options, newAnimation.options); + return existingAnimation.runner; } } else { // a joined animation means that this animation will take over the existing one // so an example would involve a leave animation taking over an enter. Then when // the postDigest kicks in the enter will be ignored. var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); if (joinAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { normalizeAnimationOptions(element, options); } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + event = newAnimation.event = existingAnimation.event; options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options); - return runner; + + //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 @@ -2233,13 +2262,12 @@ close(); clearElementAnimationState(element); return runner; } - if (isStructural) { - closeParentClassBasedAnimations(parent); - } + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); // the counter keeps track of cancelled animations var counter = (existingAnimation.counter || 0) + 1; newAnimation.counter = counter; @@ -2294,16 +2322,16 @@ // so long as a structural event did not take over the animation event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true) ? 'setClass' : animationDetails.event; - if (animationDetails.structural) { - closeParentClassBasedAnimations(parentElement); - } - markElementAnimationState(element, RUNNING_STATE); - var realRunner = $$animation(element, event, animationDetails.options); + var realRunner = $$animation(element, event, animationDetails.options, function(e) { + $$forceReflow(); + blockTransitions(getDomNode(e), false); + }); + realRunner.done(function(status) { close(!status); var animationDetails = activeAnimationsLookup.get(node); if (animationDetails && animationDetails.counter === counter) { clearElementAnimationState(getDomNode(element)); @@ -2323,10 +2351,11 @@ triggerCallback(event, 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(); runner.complete(!reject); } @@ -2359,40 +2388,13 @@ function isMatchingElement(nodeOrElmA, nodeOrElmB) { 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); - } - } - function areAnimationsAllowed(element, parentElement, event) { - var bodyElementDetected = false; - var rootElementDetected = false; + var bodyElementDetected = isMatchingElement(element, $$body) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); var parentAnimationDetected = false; var animateChildren; var parentHost = element.data(NG_ANIMATE_PIN_DATA); if (parentHost) { @@ -2443,11 +2445,11 @@ } if (!bodyElementDetected) { // we also need to ensure that the element is or will be apart of the body element // otherwise it is pointless to even issue an animation to be rendered - bodyElementDetected = isMatchingElement(parentElement, bodyElement); + bodyElementDetected = isMatchingElement(parentElement, $$body); } parentElement = parentElement.parent(); } @@ -2638,22 +2640,99 @@ function getRunner(element) { return element.data(RUNNER_STORAGE_KEY); } - this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$rAFScheduler', - function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$rAFScheduler) { + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap) { var animationQueue = []; var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - var totalPendingClassBasedAnimations = 0; - var totalActiveClassBasedAnimations = 0; - var classBasedAnimationsQueue = []; + 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: [] + }); + } + + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); + } + + return flatten(tree); + + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; + + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); + + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; + } + parentNode = parentNode.parentNode; + } + + (parentEntry || tree).children.push(entry); + return entry; + } + + function flatten(tree) { + var result = []; + var queue = []; + var i; + + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } + + 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 = result.concat(row); + row = []; + } + row.push(entry.fn); + forEach(entry.children, function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } + + if (row.length) { + result = result.concat(row); + } + return result; + } + } + // TODO(matsko): document the signature in a better way - return function(element, event, options) { + return function(element, event, options, onBeforeClassesAppliedCb) { options = prepareAnimationOptions(options); var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; // there is no animation at the current moment, however // these runner methods will get later updated with the @@ -2676,23 +2755,16 @@ if (tempClasses) { classes += ' ' + tempClasses; options.tempClasses = null; } - var classBasedIndex; - if (!isStructural) { - classBasedIndex = totalPendingClassBasedAnimations; - totalPendingClassBasedAnimations += 1; - } - animationQueue.push({ // this data is used by the postDigest code and passed into // the driver step function element: element, classes: classes, event: event, - classBasedIndex: classBasedIndex, structural: isStructural, options: options, beforeStart: beforeStart, close: close }); @@ -2703,81 +2775,71 @@ // block. This way we can group animations for all the animations that // were apart of the same postDigest flush call. 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 // form its storage. This means we can't animate this element // at all and it already has been closed due to destruction. - if (getRunner(entry.element)) { + var elm = entry.element; + if (getRunner(elm) && getDomNode(elm).parentNode) { 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 - }); + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; - 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; - }); + 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(); - $$rAFScheduler(classBasedAnimationsQueue); - } - } + var startAnimationFn, closeFn = animationEntry.close; - 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(); + // 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; - var startAnimationFn, closeFn = animationEntry.close; + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry, onBeforeClassesAppliedCb); + if (operation) { + startAnimationFn = operation.start; + } + } - // 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; + 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 parent to child classes are + // applied at the right time. + forEach(sortAnimations(toBeSortedAnimations), function(triggerAnimation) { + triggerAnimation(); }); }); return runner; @@ -2844,11 +2906,11 @@ var fromAnimation = animations[from.animationID]; var toAnimation = animations[to.animationID]; var lookupKey = from.animationID.toString(); if (!anchorGroups[lookupKey]) { var group = anchorGroups[lookupKey] = { - structural: true, + // TODO(matsko): double-check this code beforeStart: function() { fromAnimation.beforeStart(); toAnimation.beforeStart(); }, close: function() { @@ -2898,19 +2960,19 @@ } return matches.join(' '); } - function invokeFirstDriver(animationDetails) { + function invokeFirstDriver(animationDetails, onBeforeClassesAppliedCb) { // we loop in reverse order since the more general drivers (like CSS and JS) // may attempt more elements, but custom drivers are more particular for (var i = drivers.length - 1; i >= 0; i--) { var driverName = drivers[i]; if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check var factory = $injector.get(driverName); - var driver = factory(animationDetails); + var driver = factory(animationDetails, onBeforeClassesAppliedCb); if (driver) { return driver; } } } @@ -2961,12 +3023,12 @@ }]; }]; /* global angularAnimateModule: true, + $$BodyProvider, $$rAFMutexFactory, - $$rAFSchedulerFactory, $$AnimateChildrenDirective, $$AnimateRunnerFactory, $$AnimateQueueProvider, $$AnimationProvider, $AnimateCssProvider, @@ -3346,10 +3408,11 @@ * myModule.animation('.slide', ['$animateCss', function($animateCss) { * return { * enter: function(element, doneFn) { * var runner = $animateCss(element, { * event: 'enter', + * structural: true, * addClass: 'maroon-setting', * from: { height:0 }, * to: { height: 200 } * }).start(); * @@ -3699,13 +3762,14 @@ * 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`}. */ angular.module('ngAnimate', []) + .provider('$$body', $$BodyProvider) + .directive('ngAnimateChildren', $$AnimateChildrenDirective) .factory('$$rAFMutex', $$rAFMutexFactory) - .factory('$$rAFScheduler', $$rAFSchedulerFactory) .factory('$$AnimateRunner', $$AnimateRunnerFactory) .provider('$$animateQueue', $$AnimateQueueProvider) .provider('$$animation', $$AnimationProvider)