engines/bastion/vendor/assets/javascripts/bastion/angular-animate/angular-animate.js in katello-3.15.3.1 vs engines/bastion/vendor/assets/javascripts/bastion/angular-animate/angular-animate.js in katello-3.16.0.rc1

- old
+ new

@@ -1,26 +1,12 @@ /** - * @license AngularJS v1.5.5 - * (c) 2010-2016 Google, Inc. http://angularjs.org + * @license AngularJS v1.7.9 + * (c) 2010-2018 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular) {'use strict'; -/* jshint ignore:start */ -var noop = angular.noop; -var copy = angular.copy; -var extend = angular.extend; -var jqLite = angular.element; -var forEach = angular.forEach; -var isArray = angular.isArray; -var isString = angular.isString; -var isObject = angular.isObject; -var isUndefined = angular.isUndefined; -var isDefined = angular.isDefined; -var isFunction = angular.isFunction; -var isElement = angular.isElement; - var ELEMENT_NODE = 1; var COMMENT_NODE = 8; var ADD_CLASS_SUFFIX = '-add'; var REMOVE_CLASS_SUFFIX = '-remove'; @@ -41,20 +27,20 @@ // 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)) { +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 (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { +if ((window.onanimationend === undefined) && (window.onwebkitanimationend !== undefined)) { CSS_PREFIX = '-webkit-'; ANIMATION_PROP = 'WebkitAnimation'; ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; } else { ANIMATION_PROP = 'animation'; @@ -72,18 +58,14 @@ 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")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } function mergeClasses(a,b) { @@ -130,12 +112,11 @@ function stripCommentsFromElement(element) { if (element instanceof jqLite) { switch (element.length) { case 0: - return []; - break; + return element; case 1: // there is no point of stripping anything if the element // is the only element within the jqLite wrapper. // (it's important that we retain the element instance.) @@ -144,11 +125,10 @@ } break; default: return jqLite(extractElementNode(element)); - break; } } if (element.nodeType === ELEMENT_NODE) { return jqLite(element); @@ -157,11 +137,11 @@ function extractElementNode(element) { if (!element[0]) return element; for (var i = 0; i < element.length; i++) { var elm = element[i]; - if (elm.nodeType == ELEMENT_NODE) { + if (elm.nodeType === ELEMENT_NODE) { return elm; } } } @@ -185,11 +165,11 @@ } if (options.removeClass) { $$removeClass($$jqLite, element, options.removeClass); options.removeClass = null; } - } + }; } function prepareAnimationOptions(options) { options = options || {}; if (!options.$$prepared) { @@ -288,14 +268,14 @@ forEach(flags, function(val, klass) { var prop, allow; if (val === ADD_CLASS) { prop = 'addClass'; - allow = !existing[klass]; + allow = !existing[klass] || existing[klass + REMOVE_CLASS_SUFFIX]; } else if (val === REMOVE_CLASS) { prop = 'removeClass'; - allow = existing[klass]; + allow = existing[klass] || existing[klass + ADD_CLASS_SUFFIX]; } if (allow) { if (classes[prop].length) { classes[prop] += ' '; } @@ -321,14 +301,14 @@ return classes; } function getDomNode(element) { - return (element instanceof angular.element) ? element[0] : element; + return (element instanceof jqLite) ? element[0] : element; } -function applyGeneratedPreparationClasses(element, event, options) { +function applyGeneratedPreparationClasses($$jqLite, element, event, options) { var classes = ''; if (event) { classes = pendClasses(event, EVENT_CLASS_PREFIX, true); } if (options.addClass) { @@ -352,19 +332,10 @@ 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]; @@ -380,10 +351,21 @@ if (!a) return b; if (!b) return a; return a + ' ' + b; } +var helpers = { + blockTransitions: function(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]; + } +}; + var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { var queue, cancelFn; function scheduler(tasks) { // we make a copy since RAFScheduler mutates the state @@ -441,20 +423,20 @@ * * 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). + * Note that even if `ngAnimateChildren` 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"> + <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"> @@ -500,22 +482,22 @@ transform: translateX(0); } </file> <file name="script.js"> angular.module('ngAnimateChildren', ['ngAnimate']) - .controller('mainController', function() { + .controller('MainController', function MainController() { 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 + if (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)); @@ -528,10 +510,12 @@ } } }; }]; +/* exported $AnimateCssProvider */ + var ANIMATE_TIMER_KEY = '$$animateCss'; /** * @ngdoc service * @name $animateCss @@ -544,15 +528,15 @@ * directives to create more complex animations that can be purely driven using CSS code. * * Note that only browsers that support CSS transitions and/or keyframe animations are capable of * rendering animations triggered via `$animateCss` (bad news for IE9 and lower). * - * ## Usage + * ## General Use * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however, * any automatic control over cancelling animations and/or preventing animations from being run on - * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to + * child elements will not be handled by AngularJS. For this to work as expected, please use `$animate` to * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger * the CSS animation. * * The example below shows how we can create a folding animation on an element using `ng-if`: * @@ -745,11 +729,10 @@ * * * `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; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; var DETECT_CSS_PROPERTIES = { @@ -807,11 +790,11 @@ var maxValue = 0; var values = str.split(/\s*,\s*/); forEach(values, function(value) { // it's always safe to consider only second values and omit `ms` values since // getComputedStyle will always handle the conversion for us - if (value.charAt(value.length - 1) == 's') { + if (value.charAt(value.length - 1) === 's') { value = value.substring(0, value.length - 1); } value = parseFloat(value) || 0; maxValue = maxValue ? Math.max(value, maxValue) : value; }); @@ -831,37 +814,10 @@ value += ' linear all'; } return [style, value]; } -function createLocalCacheLookup() { - var cache = Object.create(null); - return { - flush: function() { - cache = Object.create(null); - }, - - count: function(key) { - var entry = cache[key]; - return entry ? entry.total : 0; - }, - - get: function(key) { - var entry = cache[key]; - return entry && entry.value; - }, - - put: function(key, value) { - if (!cache[key]) { - cache[key] = { total: 1, value: value }; - } else { - cache[key].total++; - } - } - }; -} - // 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 @@ -875,53 +831,49 @@ ? backup[prop] : node.style.getPropertyValue(prop); }); } -var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { - var gcsLookup = createLocalCacheLookup(); - var gcsStaggerLookup = createLocalCacheLookup(); +var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) { - this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', + this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$animateCache', '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', - function($window, $$jqLite, $$AnimateRunner, $timeout, + function($window, $$jqLite, $$AnimateRunner, $timeout, $$animateCache, $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - var parentCounter = 0; - function gcsHashFn(node, extraClasses) { - var KEY = "$$ngAnimateParentKey"; - var parentNode = node.parentNode; - var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); - return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; - } + function computeCachedCssStyles(node, className, cacheKey, allowNoDuration, properties) { + var timings = $$animateCache.get(cacheKey); - function computeCachedCssStyles(node, className, cacheKey, properties) { - var timings = gcsLookup.get(cacheKey); - if (!timings) { timings = computeCssStyles($window, node, properties); if (timings.animationIterationCount === 'infinite') { timings.animationIterationCount = 1; } } + // if a css animation has no duration we + // should mark that so that repeated addClass/removeClass calls are skipped + var hasDuration = allowNoDuration || (timings.transitionDuration > 0 || timings.animationDuration > 0); + // we keep putting this in multiple times even though the value and the cacheKey are the same // because we're keeping an internal tally of how many duplicate animations are detected. - gcsLookup.put(cacheKey, timings); + $$animateCache.put(cacheKey, timings, hasDuration); + return timings; } function computeCachedCssStaggerStyles(node, className, cacheKey, properties) { var stagger; + var staggerCacheKey = 'stagger-' + cacheKey; // if we have one or more existing matches of matching elements // containing the same parent + CSS styles (which is how cacheKey works) // then staggering is possible - if (gcsLookup.count(cacheKey) > 0) { - stagger = gcsStaggerLookup.get(cacheKey); + if ($$animateCache.count(cacheKey) > 0) { + stagger = $$animateCache.get(staggerCacheKey); if (!stagger) { var staggerClassName = pendClasses(className, '-stagger'); $$jqLite.addClass(node, staggerClassName); @@ -932,24 +884,22 @@ stagger.animationDuration = Math.max(stagger.animationDuration, 0); stagger.transitionDuration = Math.max(stagger.transitionDuration, 0); $$jqLite.removeClass(node, staggerClassName); - gcsStaggerLookup.put(cacheKey, stagger); + $$animateCache.put(staggerCacheKey, stagger, true); } } return stagger || {}; } - var cancelLastRAFRequest; var rafWaitQueue = []; function waitUntilQuiet(callback) { rafWaitQueue.push(callback); $$rAFScheduler.waitUntilQuiet(function() { - gcsLookup.flush(); - gcsStaggerLookup.flush(); + $$animateCache.flush(); // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. // PLEASE EXAMINE THE `$$forceReflow` service to understand why. var pageWidth = $$forceReflow(); @@ -960,12 +910,12 @@ } rafWaitQueue.length = 0; }); } - function computeTimings(node, className, cacheKey) { - var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); + function computeTimings(node, className, cacheKey, allowNoDuration) { + var timings = computeCachedCssStyles(node, className, cacheKey, allowNoDuration, DETECT_CSS_PROPERTIES); var aD = timings.animationDelay; var tD = timings.transitionDelay; timings.maxDelay = aD && tD ? Math.max(aD, tD) : (aD || tD); @@ -1048,11 +998,10 @@ applyAnimationClasses(element, options); } 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, @@ -1061,21 +1010,25 @@ && !hasToStyles && !preparationClasses) { return closeAndReturnNoopAnimator(); } - var cacheKey, stagger; + var stagger, cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass); + if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) { + preparationClasses = null; + return closeAndReturnNoopAnimator(); + } + if (options.stagger > 0) { var staggerVal = parseFloat(options.stagger); stagger = { transitionDelay: staggerVal, animationDelay: staggerVal, transitionDuration: 0, animationDuration: 0 }; } else { - cacheKey = gcsHashFn(node, fullClassName); stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); } if (!options.$$skipPreparationClasses) { $$jqLite.addClass(element, preparationClasses); @@ -1105,11 +1058,11 @@ } var itemIndex = stagger ? options.staggerIndex >= 0 ? options.staggerIndex - : gcsLookup.count(cacheKey) + : $$animateCache.count(cacheKey) : 0; var isFirst = itemIndex === 0; // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY @@ -1117,22 +1070,22 @@ // 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 && !options.skipBlocking) { - blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); + helpers.blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); } - var timings = computeTimings(node, fullClassName, cacheKey); + var timings = computeTimings(node, fullClassName, cacheKey, !isStructural); var relativeDelay = timings.maxDelay; maxDelay = Math.max(relativeDelay, 0); maxDuration = timings.maxDuration; var flags = {}; flags.hasTransitions = timings.transitionDuration > 0; flags.hasAnimations = timings.animationDuration > 0; - flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all'; + flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty === 'all'; flags.applyTransitionDuration = hasToStyles && ( (flags.hasTransitions && !flags.hasTransitionAll) || (flags.hasAnimations && !flags.hasTransitions)); flags.applyAnimationDuration = options.duration && flags.hasAnimations; flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions); @@ -1158,13 +1111,15 @@ if (maxDuration === 0 && !flags.recalculateTimingStyles) { return closeAndReturnNoopAnimator(); } + var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); + if (options.delay != null) { var delayStyle; - if (typeof options.delay !== "boolean") { + 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); } @@ -1201,11 +1156,11 @@ } if (flags.blockTransition || flags.blockKeyframeAnimation) { applyBlocking(maxDuration); } else if (!options.skipBlocking) { - blockTransitions(node, false); + helpers.blockTransitions(node, false); } // TODO(matsko): for 1.5 change this code to have an animator object for better debugging return { $$willAnimate: true, @@ -1238,24 +1193,27 @@ function cancelFn() { close(true); } - function close(rejected) { // jshint ignore:line + function close(rejected) { // if the promise has been called already then we shouldn't close // the animation again if (animationClosed || (animationCompleted && animationPaused)) return; animationClosed = true; animationPaused = false; - if (!options.$$skipPreparationClasses) { + if (preparationClasses && !options.$$skipPreparationClasses) { $$jqLite.removeClass(element, preparationClasses); } - $$jqLite.removeClass(element, activeClasses); + if (activeClasses) { + $$jqLite.removeClass(element, activeClasses); + } + blockKeyframeAnimations(node, false); - blockTransitions(node, false); + helpers.blockTransitions(node, false); forEach(temporaryStyles, function(entry) { // There is only one way to remove inline style properties entirely from elements. // By using `removeProperty` this works, but we need to convert camel-cased CSS // styles down to hyphenated values. @@ -1265,12 +1223,15 @@ 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); + if (value) { + node.style.setProperty(prop, value); + } else { + 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 @@ -1299,11 +1260,11 @@ } } function applyBlocking(duration) { if (flags.blockTransition) { - blockTransitions(node, duration); + helpers.blockTransitions(node, duration); } if (flags.blockKeyframeAnimation) { blockKeyframeAnimations(node, !!duration); } @@ -1330,10 +1291,16 @@ function onAnimationProgress(event) { event.stopPropagation(); var ev = event.originalEvent || event; + if (ev.target !== node) { + // Since TransitionEvent / AnimationEvent bubble up, + // we have to ignore events by finished child animations + return; + } + // 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 @@ -1369,13 +1336,15 @@ var playPause = function(playAnimation) { if (!animationCompleted) { animationPaused = !playAnimation; if (timings.animationDuration) { var value = blockKeyframeAnimations(node, animationPaused); - animationPaused - ? temporaryStyles.push(value) - : removeFromArray(temporaryStyles, value); + if (animationPaused) { + temporaryStyles.push(value); + } else { + removeFromArray(temporaryStyles, value); + } } } else if (animationPaused && playAnimation) { animationPaused = false; close(); } @@ -1420,14 +1389,14 @@ applyAnimationClasses(element, options); $$jqLite.addClass(element, activeClasses); if (flags.recalculateTimingStyles) { - fullClassName = node.className + ' ' + preparationClasses; - cacheKey = gcsHashFn(node, fullClassName); + fullClassName = node.getAttribute('class') + ' ' + preparationClasses; + cacheKey = $$animateCache.cacheKey(node, method, options.addClass, options.removeClass); - timings = computeTimings(node, fullClassName, cacheKey); + timings = computeTimings(node, fullClassName, cacheKey, false); relativeDelay = timings.maxDelay; maxDelay = Math.max(relativeDelay, 0); maxDuration = timings.maxDuration; if (maxDuration === 0) { @@ -1438,11 +1407,11 @@ flags.hasTransitions = timings.transitionDuration > 0; flags.hasAnimations = timings.animationDuration > 0; } if (flags.applyAnimationDelay) { - relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + relativeDelay = typeof options.delay !== 'boolean' && truthyTimingValue(options.delay) ? parseFloat(options.delay) : relativeDelay; maxDelay = Math.max(relativeDelay, 0); timings.animationDelay = relativeDelay; @@ -1530,11 +1499,11 @@ } }; }]; }]; -var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { +var $$AnimateCssDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) { $$animationProvider.drivers.push('$$animateCssDriver'); var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; @@ -1559,12 +1528,10 @@ // we also special case the doc fragment 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 ? prepareFromToAnchorAnimation(animationDetails.from, animationDetails.to, animationDetails.classes, @@ -1802,11 +1769,11 @@ // TODO(matsko): use caching here to speed things up for detection // TODO(matsko): add documentation // by the time... -var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { +var $$AnimateJsProvider = ['$animateProvider', /** @this */ function($animateProvider) { this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', function($injector, $$AnimateRunner, $$jqLite) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); // $animateJs(element, 'enter'); @@ -1841,11 +1808,11 @@ // we don't return anything which then makes $animation query the next driver. var animations = lookupAnimations(classes); var before, after; if (animations.length) { var afterFn, beforeFn; - if (event == 'leave') { + if (event === 'leave') { beforeFn = 'leave'; afterFn = 'afterLeave'; // TODO(matsko): get rid of this } else { beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1); afterFn = event; @@ -2026,11 +1993,11 @@ } function packageAnimations(element, event, options, animations, fnName) { var operations = groupEventedAnimations(element, event, options, animations, fnName); if (operations.length === 0) { - var a,b; + var a, b; if (fnName === 'beforeSetClass') { a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); } else if (fnName === 'setClass') { a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass'); @@ -2054,25 +2021,33 @@ forEach(operations, function(animateFn) { runners.push(animateFn()); }); } - runners.length ? $$AnimateRunner.all(runners, callback) : callback(); + if (runners.length) { + $$AnimateRunner.all(runners, callback); + } else { + callback(); + } return function endFn(reject) { forEach(runners, function(runner) { - reject ? runner.cancel() : runner.end(); + if (reject) { + runner.cancel(); + } else { + runner.end(); + } }); }; }; } }; function lookupAnimations(classes) { classes = isArray(classes) ? classes : classes.split(' '); var matches = [], flagMap = {}; - for (var i=0; i < classes.length; i++) { + for (var i = 0; i < classes.length; i++) { var klass = classes[i], animationFactory = $animateProvider.$$registeredAnimations[klass]; if (animationFactory && !flagMap[klass]) { matches.push($injector.get(animationFactory)); flagMap[klass] = true; @@ -2081,11 +2056,11 @@ return matches; } }]; }]; -var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { +var $$AnimateJsDriverProvider = ['$$animationProvider', /** @this */ function($$animationProvider) { $$animationProvider.drivers.push('$$animateJsDriver'); this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { return function initDriverFn(animationDetails) { if (animationDetails.from && animationDetails.to) { var fromAnimation = prepareAnimation(animationDetails.from); @@ -2143,21 +2118,30 @@ }]; }]; var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; -var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { +var $$AnimateQueueProvider = ['$animateProvider', /** @this */ function($animateProvider) { var PRE_DIGEST_STATE = 1; var RUNNING_STATE = 2; var ONE_SPACE = ' '; var rules = this.rules = { skip: [], cancel: [], join: [] }; + function getEventData(options) { + return { + addClass: options.addClass, + removeClass: options.removeClass, + from: options.from, + to: options.to + }; + } + function makeTruthyCssClassMap(classString) { if (!classString) { return null; } @@ -2177,56 +2161,56 @@ return currentClassMap[className]; }); } } - function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + function isAllowed(ruleType, currentAnimation, previousAnimation) { return rules[ruleType].some(function(fn) { - return fn(element, currentAnimation, previousAnimation); + return fn(currentAnimation, previousAnimation); }); } 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) { + rules.join.push(function(newAnimation, currentAnimation) { // if the new animation is class-based then we can just tack that on return !newAnimation.structural && hasAnimationClasses(newAnimation); }); - rules.skip.push(function(element, newAnimation, currentAnimation) { + rules.skip.push(function(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); }); - rules.skip.push(function(element, newAnimation, currentAnimation) { + rules.skip.push(function(newAnimation, currentAnimation) { // why should we trigger a new structural animation if the element will // be removed from the DOM anyway? - return currentAnimation.event == 'leave' && newAnimation.structural; + return currentAnimation.event === 'leave' && newAnimation.structural; }); - rules.skip.push(function(element, newAnimation, currentAnimation) { + rules.skip.push(function(newAnimation, currentAnimation) { // 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) { + rules.cancel.push(function(newAnimation, currentAnimation) { // there can never be two structural animations running at the same time return currentAnimation.structural && newAnimation.structural; }); - rules.cancel.push(function(element, newAnimation, currentAnimation) { + rules.cancel.push(function(newAnimation, currentAnimation) { // if the previous animation is already running, but the new animation will // be triggered, but the new animation is structural return currentAnimation.state === RUNNING_STATE && newAnimation.structural; }); - rules.cancel.push(function(element, newAnimation, currentAnimation) { + rules.cancel.push(function(newAnimation, currentAnimation) { // cancel the animation if classes added / removed in both animation cancel each other out, // but only if the current animation isn't structural if (currentAnimation.structural) return false; @@ -2241,19 +2225,25 @@ } return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); }); - this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$Map', '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', - function($$rAF, $rootScope, $rootElement, $document, $$HashMap, - $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + '$$isDocumentHidden', + function($$rAF, $rootScope, $rootElement, $document, $$Map, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow, + $$isDocumentHidden) { - var activeAnimationsLookup = new $$HashMap(); - var disabledElementsLookup = new $$HashMap(); + var activeAnimationsLookup = new $$Map(); + var disabledElementsLookup = new $$Map(); var animationsEnabled = null; + function removeFromDisabledElementsLookup(evt) { + disabledElementsLookup.delete(evt.target); + } + function postDigestTaskFactory() { var postDigestCalled = false; return function(fn) { // we only issue a call to postDigest before // it has first passed. This prevents any callbacks @@ -2297,38 +2287,37 @@ }); }); } ); - var callbackRegistry = {}; + var callbackRegistry = Object.create(null); - // remember that the classNameFilter is set during the provider/config - // stage therefore we can optimize here and setup a helper function + // remember that the `customFilter`/`classNameFilter` are set during the + // provider/config stage therefore we can optimize here and setup helper functions + var customFilter = $animateProvider.customFilter(); var classNameFilter = $animateProvider.classNameFilter(); - var isAnimatableClassName = !classNameFilter - ? function() { return true; } - : function(className) { - return classNameFilter.test(className); - }; + var returnTrue = function() { return true; }; + var isAnimatableByFilter = customFilter || returnTrue; + var isAnimatableClassName = !classNameFilter ? returnTrue : function(node, options) { + var className = [node.getAttribute('class'), options.addClass, options.removeClass].join(' '); + return classNameFilter.test(className); + }; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); function normalizeAnimationDetails(element, animation) { return mergeAnimationDetails(element, animation, {}); } // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. - var contains = window.Node.prototype.contains || function(arg) { - // jshint bitwise: false + var contains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise return this === arg || !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true }; - function findCallbacks(parent, element, event) { - var targetNode = getDomNode(element); - var targetParentNode = getDomNode(parent); - + function findCallbacks(targetParentNode, targetNode, event) { var matches = []; var entries = callbackRegistry[event]; if (entries) { forEach(entries, function(entry) { if (contains.call(entry.node, targetNode)) { @@ -2349,15 +2338,15 @@ (!matchCallback || entry.callback === matchCallback); return !isMatch; }); } - function cleanupEventListeners(phase, element) { - if (phase === 'close' && !element[0].parentNode) { + function cleanupEventListeners(phase, node) { + if (phase === 'close' && !node.parentNode) { // If the element is not attached to a parentNode, it has been removed by // the domOperation, and we can safely remove the event callbacks - $animate.off(element); + $animate.off(node); } } var $animate = { on: function(event, container, callback) { @@ -2380,11 +2369,11 @@ } }); }, off: function(event, container, callback) { - if (arguments.length === 1 && !angular.isString(arguments[0])) { + if (arguments.length === 1 && !isString(arguments[0])) { container = arguments[0]; for (var eventType in callbackRegistry) { callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container); } @@ -2428,40 +2417,41 @@ if (!hasElement) { // (bool) - Global setter bool = animationsEnabled = !!element; } else { var node = getDomNode(element); - var recordExists = disabledElementsLookup.get(node); if (argCount === 1) { // (element) - Element getter - bool = !recordExists; + bool = !disabledElementsLookup.get(node); } else { // (element, bool) - Element setter - disabledElementsLookup.put(node, !bool); + if (!disabledElementsLookup.has(node)) { + // The element is added to the map for the first time. + // Create a listener to remove it on `$destroy` (to avoid memory leak). + jqLite(element).on('$destroy', removeFromDisabledElementsLookup); + } + disabledElementsLookup.set(node, !bool); } } } return bool; } }; return $animate; - function queueAnimation(element, event, initialOptions) { + function queueAnimation(originalElement, 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) { - node = getDomNode(element); - parent = element.parent(); - } + var element = stripCommentsFromElement(originalElement); + var node = getDomNode(element); + var parentNode = node && node.parentNode; options = prepareAnimationOptions(options); // we create a fake runner with a working promise. // These methods will become available after the digest has passed @@ -2492,53 +2482,49 @@ 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) { + // If animations are hard-disabled for the whole application there is no need to continue. + // There are also situations where a directive issues an animation for a jqLite wrapper that + // contains only comment nodes. In this case, there is no way we can perform an animation. + if (!animationsEnabled || + !node || + !isAnimatableByFilter(node, event, initialOptions) || + !isAnimatableClassName(node, options)) { close(); return runner; } - var className = [node.className, options.addClass, options.removeClass].join(' '); - if (!isAnimatableClassName(className)) { - close(); - return runner; - } - var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; - var documentHidden = $document[0].hidden; + var documentHidden = $$isDocumentHidden(); - // 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 + // This is a hard disable of all animations the element itself, therefore there is no need to + // continue further past this point if not enabled // 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 || documentHidden || disabledElementsLookup.get(node); + var skipAnimations = documentHidden || disabledElementsLookup.get(node); var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; var hasExistingAnimation = !!existingAnimation.state; // there is no point in traversing the same collection of parent ancestors if a followup // animation will be run on the same element that already did all that checking work - if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) { - skipAnimations = !areAnimationsAllowed(element, parent, event); + if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state !== PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(node, parentNode, event); } if (skipAnimations) { // Callbacks should fire even if the document is hidden (regression fix for issue #14120) - if (documentHidden) notifyProgress(runner, event, 'start'); + if (documentHidden) notifyProgress(runner, event, 'start', getEventData(options)); close(); - if (documentHidden) notifyProgress(runner, event, 'close'); + if (documentHidden) notifyProgress(runner, event, 'close', getEventData(options)); return runner; } if (isStructural) { - closeChildAnimations(element); + closeChildAnimations(node); } var newAnimation = { structural: isStructural, element: element, @@ -2549,21 +2535,21 @@ options: options, runner: runner }; if (hasExistingAnimation) { - var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + var skipAnimationFlag = isAllowed('skip', newAnimation, existingAnimation); if (skipAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { close(); return runner; } else { mergeAnimationDetails(element, existingAnimation, newAnimation); return existingAnimation.runner; } } - var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + var cancelAnimationFlag = isAllowed('cancel', newAnimation, existingAnimation); if (cancelAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { // this will end the animation right away and it is safe // to do so since the animation is already running and the // runner callback code will run in async @@ -2581,16 +2567,16 @@ } } 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); + var joinAnimationFlag = isAllowed('join', newAnimation, existingAnimation); if (joinAnimationFlag) { if (existingAnimation.state === RUNNING_STATE) { normalizeAnimationDetails(element, newAnimation); } else { - applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + applyGeneratedPreparationClasses($$jqLite, element, isStructural ? event : null, options); event = newAnimation.event = existingAnimation.event; options = mergeAnimationDetails(element, existingAnimation, newAnimation); //we return the same runner since only the option values of this animation will @@ -2615,21 +2601,30 @@ || hasAnimationClasses(newAnimation); } if (!isValidAnimation) { close(); - clearElementAnimationState(element); + clearElementAnimationState(node); return runner; } // the counter keeps track of cancelled animations var counter = (existingAnimation.counter || 0) + 1; newAnimation.counter = counter; - markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + markElementAnimationState(node, PRE_DIGEST_STATE, newAnimation); $rootScope.$$postDigest(function() { + // It is possible that the DOM nodes inside `originalElement` have been replaced. This can + // happen if the animated element is a transcluded clone and also has a `templateUrl` + // directive on it. Therefore, we must recreate `element` in order to interact with the + // actual DOM nodes. + // Note: We still need to use the old `node` for certain things, such as looking up in + // HashMaps where it was used as the key. + + element = stripCommentsFromElement(originalElement); + var animationDetails = activeAnimationsLookup.get(node); var animationCancelled = !animationDetails; animationDetails = animationDetails || {}; // if addClass/removeClass is called before something like enter then the @@ -2664,11 +2659,11 @@ // in the event that the element animation was not cancelled or a follow-up animation // isn't allowed to animate from here then we need to clear the state of the element // so that any future animations won't read the expired animation data. if (!isValidAnimation) { - clearElementAnimationState(element); + clearElementAnimationState(node); } return; } @@ -2676,203 +2671,255 @@ // so long as a structural event did not take over the animation event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) ? 'setClass' : animationDetails.event; - markElementAnimationState(element, RUNNING_STATE); + markElementAnimationState(node, RUNNING_STATE); var realRunner = $$animation(element, event, animationDetails.options); // this will update the runner's flow-control events based on // the `realRunner` object. runner.setHost(realRunner); - notifyProgress(runner, event, 'start', {}); + notifyProgress(runner, event, 'start', getEventData(options)); realRunner.done(function(status) { close(!status); var animationDetails = activeAnimationsLookup.get(node); if (animationDetails && animationDetails.counter === counter) { - clearElementAnimationState(getDomNode(element)); + clearElementAnimationState(node); } - notifyProgress(runner, event, 'close', {}); + notifyProgress(runner, event, 'close', getEventData(options)); }); }); return runner; function notifyProgress(runner, event, phase, data) { runInNextPostDigestOrNow(function() { - var callbacks = findCallbacks(parent, element, event); + var callbacks = findCallbacks(parentNode, node, 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); }); - cleanupEventListeners(phase, element); + cleanupEventListeners(phase, node); }); } else { - cleanupEventListeners(phase, element); + cleanupEventListeners(phase, node); } }); runner.progress(event, phase, data); } - function close(reject) { // jshint ignore:line + function close(reject) { clearGeneratedClasses(element, options); applyAnimationClasses(element, options); applyAnimationStyles(element, options); options.domOperation(); runner.complete(!reject); } } - function closeChildAnimations(element) { - var node = getDomNode(element); + function closeChildAnimations(node) { var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); forEach(children, function(child) { - var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME), 10); var animationDetails = activeAnimationsLookup.get(child); if (animationDetails) { switch (state) { case RUNNING_STATE: animationDetails.runner.end(); /* falls through */ case PRE_DIGEST_STATE: - activeAnimationsLookup.remove(child); + activeAnimationsLookup.delete(child); break; } } }); } - function clearElementAnimationState(element) { - var node = getDomNode(element); + function clearElementAnimationState(node) { node.removeAttribute(NG_ANIMATE_ATTR_NAME); - activeAnimationsLookup.remove(node); + activeAnimationsLookup.delete(node); } - function isMatchingElement(nodeOrElmA, nodeOrElmB) { - return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); - } - /** * 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 bodyElement = jqLite($document[0].body); - var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; - var rootElementDetected = isMatchingElement(element, $rootElement); + function areAnimationsAllowed(node, parentNode, event) { + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var bodyNodeDetected = (node === bodyNode) || node.nodeName === 'HTML'; + var rootNodeDetected = (node === rootNode); var parentAnimationDetected = false; + var elementDisabled = disabledElementsLookup.get(node); var animateChildren; - var elementDisabled = disabledElementsLookup.get(getDomNode(element)); - var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + var parentHost = jqLite.data(node, NG_ANIMATE_PIN_DATA); if (parentHost) { - parentElement = parentHost; + parentNode = getDomNode(parentHost); } - parentElement = getDomNode(parentElement); - - while (parentElement) { - if (!rootElementDetected) { - // angular doesn't want to attempt to animate elements outside of the application + while (parentNode) { + if (!rootNodeDetected) { + // AngularJS 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); + rootNodeDetected = (parentNode === rootNode); } - if (parentElement.nodeType !== ELEMENT_NODE) { + if (parentNode.nodeType !== ELEMENT_NODE) { // no point in inspecting the #document element break; } - var details = activeAnimationsLookup.get(parentElement) || {}; + var details = activeAnimationsLookup.get(parentNode) || {}; // 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) { - var parentElementDisabled = disabledElementsLookup.get(parentElement); + var parentNodeDisabled = disabledElementsLookup.get(parentNode); - if (parentElementDisabled === true && elementDisabled !== false) { + if (parentNodeDisabled === 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) { + } else if (parentNodeDisabled === false) { elementDisabled = false; } parentAnimationDetected = details.structural; } if (isUndefined(animateChildren) || animateChildren === true) { - var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + var value = jqLite.data(parentNode, NG_ANIMATE_CHILDREN_DATA); if (isDefined(value)) { animateChildren = value; } } // there is no need to continue traversing at this point if (parentAnimationDetected && animateChildren === false) break; - if (!bodyElementDetected) { + if (!bodyNodeDetected) { // 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); + bodyNodeDetected = (parentNode === bodyNode); } - if (bodyElementDetected && rootElementDetected) { + if (bodyNodeDetected && rootNodeDetected) { // 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 (!rootNodeDetected) { + // If `rootNode` is not detected, check if `parentNode` is pinned to another element + parentHost = jqLite.data(parentNode, NG_ANIMATE_PIN_DATA); if (parentHost) { // The pin target element becomes the next parent element - parentElement = getDomNode(parentHost); + parentNode = getDomNode(parentHost); continue; } } - parentElement = parentElement.parentNode; + parentNode = parentNode.parentNode; } var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; - return allowAnimation && rootElementDetected && bodyElementDetected; + return allowAnimation && rootNodeDetected && bodyNodeDetected; } - function markElementAnimationState(element, state, details) { + function markElementAnimationState(node, state, details) { details = details || {}; details.state = state; - var node = getDomNode(element); node.setAttribute(NG_ANIMATE_ATTR_NAME, state); var oldValue = activeAnimationsLookup.get(node); var newValue = oldValue ? extend(oldValue, details) : details; - activeAnimationsLookup.put(node, newValue); + activeAnimationsLookup.set(node, newValue); } }]; }]; -var $$AnimationProvider = ['$animateProvider', function($animateProvider) { +/** @this */ +var $$AnimateCacheProvider = function() { + + var KEY = '$$ngAnimateParentKey'; + var parentCounter = 0; + var cache = Object.create(null); + + this.$get = [function() { + return { + cacheKey: function(node, method, addClass, removeClass) { + var parentNode = node.parentNode; + var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); + var parts = [parentID, method, node.getAttribute('class')]; + if (addClass) { + parts.push(addClass); + } + if (removeClass) { + parts.push(removeClass); + } + return parts.join(' '); + }, + + containsCachedAnimationWithoutDuration: function(key) { + var entry = cache[key]; + + // nothing cached, so go ahead and animate + // otherwise it should be a valid animation + return (entry && !entry.isValid) || false; + }, + + flush: function() { + cache = Object.create(null); + }, + + count: function(key) { + var entry = cache[key]; + return entry ? entry.total : 0; + }, + + get: function(key) { + var entry = cache[key]; + return entry && entry.value; + }, + + put: function(key, value, isValid) { + if (!cache[key]) { + cache[key] = { total: 1, value: value, isValid: isValid }; + } else { + cache[key].total++; + cache[key].value = value; + } + } + }; + }]; +}; + +/* exported $$AnimationProvider */ + +var $$AnimationProvider = ['$animateProvider', /** @this */ function($animateProvider) { var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; var drivers = this.drivers = []; var RUNNER_STORAGE_KEY = '$$animationRunner'; + var PREPARE_CLASSES_KEY = '$$animatePrepareClasses'; function setRunner(element, runner) { element.data(RUNNER_STORAGE_KEY, runner); } @@ -2882,26 +2929,27 @@ function getRunner(element) { return element.data(RUNNER_STORAGE_KEY); } - this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', - function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$Map', '$$rAFScheduler', '$$animateCache', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$Map, $$rAFScheduler, $$animateCache) { var animationQueue = []; var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); function sortAnimations(animations) { var tree = { children: [] }; - var i, lookup = new $$HashMap(); + var i, lookup = new $$Map(); - // this is done first beforehand so that the hashmap + // this is done first beforehand so that the map // 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] = { + lookup.set(animation.domNode, animations[i] = { domNode: animation.domNode, + element: animation.element, fn: animation.fn, children: [] }); } @@ -2915,11 +2963,11 @@ if (entry.processed) return entry; entry.processed = true; var elementNode = entry.domNode; var parentNode = elementNode.parentNode; - lookup.put(elementNode, entry); + lookup.set(elementNode, entry); var parentEntry; while (parentNode) { parentEntry = lookup.get(parentNode); if (parentEntry) { @@ -2954,11 +3002,11 @@ remainingLevelEntries = nextLevelEntries; nextLevelEntries = 0; result.push(row); row = []; } - row.push(entry.fn); + row.push(entry); entry.children.forEach(function(childEntry) { nextLevelEntries++; queue.push(childEntry); }); remainingLevelEntries--; @@ -2989,25 +3037,23 @@ if (!drivers.length) { close(); return runner; } - setRunner(element, runner); - var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); var tempClasses = options.tempClasses; if (tempClasses) { classes += ' ' + tempClasses; options.tempClasses = null; } - var prepareClassName; if (isStructural) { - prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; - $$jqLite.addClass(element, prepareClassName); + element.data(PREPARE_CLASSES_KEY, 'ng-' + event + PREPARE_CLASS_SUFFIX); } + setRunner(element, runner); + animationQueue.push({ // this data is used by the postDigest code and passed into // the driver step function element: element, classes: classes, @@ -3043,20 +3089,35 @@ var groupedAnimations = groupAnimations(animations); var toBeSortedAnimations = []; forEach(groupedAnimations, function(animationEntry) { + var element = animationEntry.from ? animationEntry.from.element : animationEntry.element; + var extraClasses = options.addClass; + + extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME; + var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass); + toBeSortedAnimations.push({ - domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + element: element, + domNode: getDomNode(element), fn: function triggerAnimationStart() { + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that we've cached the animation status for this element + // and it's in fact an invalid animation (something that has duration = 0) + // then we should skip all the heavy work from here on + if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) { + closeFn(); + return; + } + // 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; @@ -3082,11 +3143,36 @@ }); // 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)); + var finalAnimations = sortAnimations(toBeSortedAnimations); + for (var i = 0; i < finalAnimations.length; i++) { + var innerArray = finalAnimations[i]; + for (var j = 0; j < innerArray.length; j++) { + var entry = innerArray[j]; + var element = entry.element; + + // the RAFScheduler code only uses functions + finalAnimations[i][j] = entry.fn; + + // the first row of elements shouldn't have a prepare-class added to them + // since the elements are at the top of the animation hierarchy and they + // will be applied without a RAF having to pass... + if (i === 0) { + element.removeData(PREPARE_CLASSES_KEY); + continue; + } + + var prepareClassName = element.data(PREPARE_CLASSES_KEY); + if (prepareClassName) { + $$jqLite.addClass(element, prepareClassName); + } + } + } + + $$rAFScheduler(finalAnimations); }); return runner; // TODO(matsko): change to reference nodes @@ -3211,25 +3297,23 @@ function invokeFirstDriver(animationDetails) { // 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); if (driver) { return driver; } } } function beforeStart() { - element.addClass(NG_ANIMATE_CLASSNAME); - if (tempClasses) { - $$jqLite.addClass(element, tempClasses); - } + tempClasses = (tempClasses ? (tempClasses + ' ') : '') + NG_ANIMATE_CLASSNAME; + $$jqLite.addClass(element, tempClasses); + + var prepareClassName = element.data(PREPARE_CLASSES_KEY); if (prepareClassName) { $$jqLite.removeClass(element, prepareClassName); prepareClassName = null; } } @@ -3241,22 +3325,23 @@ } else { update(animation.element); } function update(element) { - getRunner(element).setHost(newRunner); + var runner = getRunner(element); + if (runner) runner.setHost(newRunner); } } function handleDestroyedElement() { var runner = getRunner(element); if (runner && (event !== 'leave' || !options.$$domOperationFired)) { runner.end(); } } - function close(rejected) { // jshint ignore:line + function close(rejected) { element.off('$destroy', handleDestroyedElement); removeRunner(element); applyAnimationClasses(element, options); applyAnimationStyles(element, options); @@ -3264,11 +3349,10 @@ if (tempClasses) { $$jqLite.removeClass(element, tempClasses); } - element.removeClass(NG_ANIMATE_CLASSNAME); runner.complete(!rejected); } }; }]; }]; @@ -3358,16 +3442,17 @@ * .yellow { background:yellow; } * .orange { background:orange; } * </file> * </example> */ -var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) { +var ngAnimateSwapDirective = ['$animate', function($animate) { return { restrict: 'A', transclude: 'element', terminal: true, - priority: 600, // we use 600 here to ensure that the directive is caught before others + priority: 550, // We use 550 here to ensure that the directive is caught before others, + // but after `ngIf` (at priority 600). link: function(scope, $element, attrs, ctrl, $transclude) { var previousElement, previousScope; scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) { if (previousElement) { $animate.leave(previousElement); @@ -3375,73 +3460,65 @@ if (previousScope) { previousScope.$destroy(); previousScope = null; } if (value || value === 0) { - previousScope = scope.$new(); - $transclude(previousScope, function(element) { - previousElement = element; - $animate.enter(element, null, $element); + $transclude(function(clone, childScope) { + previousElement = clone; + previousScope = childScope; + $animate.enter(clone, null, $element); }); } }); } }; }]; -/* global angularAnimateModule: true, - - ngAnimateSwapDirective, - $$AnimateAsyncRunFactory, - $$rAFSchedulerFactory, - $$AnimateChildrenDirective, - $$AnimateQueueProvider, - $$AnimationProvider, - $AnimateCssProvider, - $$AnimateCssDriverProvider, - $$AnimateJsProvider, - $$AnimateJsDriverProvider, -*/ - /** * @ngdoc module * @name ngAnimate * @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` 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 AngularJS app. * - * <div doc-module-components="ngAnimate"></div> - * - * # Usage + * ## Usage * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within * the HTML element that the animation will be triggered on. * * ## Directive Support * The following directives are "animation aware": * - * | Directive | Supported Animations | - * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| - * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | - * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | - * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | - * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | - * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | - * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * | Directive | Supported Animations | + * |-------------------------------------------------------------------------------|---------------------------------------------------------------------------| + * | {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) | + * | {@link ngAnimate.directive:ngAnimateSwap#animations ngAnimateSwap} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass / {{class&#125;&#8203;&#125;} | add and remove | + * | {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove | + * | {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove | + * | {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave | + * | {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) | + * | {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) | + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave, and move | + * | {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | * - * (More information can be found by visiting each the documentation associated with each directive.) + * (More information can be found by visiting the documentation associated with each directive.) * + * For a full breakdown of the steps involved during each animation event, refer to the + * {@link ng.$animate `$animate` API docs}. + * * ## CSS-based Animations * * 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. + * and CSS code we can create an animation that will be picked up by AngularJS when an underlying directive performs an operation. * * 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"> @@ -3577,10 +3654,14 @@ * transition-delay: 0.1s; * * /&#42; As of 1.4.4, this must always be set: it signals ngAnimate * to not accidentally inherit a delay property from another CSS class &#42;/ * transition-duration: 0s; + * + * /&#42; if you are using animations instead of transitions you should configure as follows: + * animation-delay: 0.1s; + * animation-duration: 0s; &#42;/ * } * .my-animation.ng-enter.ng-enter-active { * /&#42; standard transition styles &#42;/ * opacity:1; * } @@ -3665,13 +3746,26 @@ * * ```css * .message.ng-enter-prepare { * opacity: 0; * } - * * ``` * + * ### Animating between value changes + * + * Sometimes you need to animate between different expression states, whose values + * don't necessary need to be known or referenced in CSS styles. + * Unless possible with another {@link ngAnimate#directive-support "animation aware" directive}, + * that specific use case can always be covered with {@link ngAnimate.directive:ngAnimateSwap} as + * can be seen in {@link ngAnimate.directive:ngAnimateSwap#examples this example}. + * + * Note that {@link ngAnimate.directive:ngAnimateSwap} is a *structural directive*, which means it + * creates a new instance of the element (including any other/child directives it may have) and + * links it to a new scope every time *swap* happens. In some cases this might not be desirable + * (e.g. for performance reasons, or when you wish to retain internal state on the original + * element instance). + * * ## 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 * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the * `module.animation()` module function we can register the animation. @@ -3692,11 +3786,11 @@ * // make note that other events (like addClass/removeClass) * // have different function input parameters * enter: function(element, doneFn) { * jQuery(element).fadeIn(1000, doneFn); * - * // remember to call doneFn so that angular + * // remember to call doneFn so that AngularJS * // knows that the animation has concluded * }, * * move: function(element, doneFn) { * jQuery(element).fadeIn(1000, doneFn); @@ -3740,11 +3834,11 @@ * }]); * ``` * * ## CSS + JS Animations Together * - * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular, + * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of AngularJS, * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking * charge of the animation**: * * ```html * <div ng-if="bool" class="slide"> @@ -3932,11 +4026,11 @@ name="anchoringExample" id="anchoringExample" deps="angular-animate.js;angular-route.js" animations="true"> <file name="index.html"> - <a href="#/">Home</a> + <a href="#!/">Home</a> <hr /> <div class="view-container"> <div ng-view class="view"></div> </div> </file> @@ -3952,26 +4046,27 @@ controller: 'ProfileController as profile' }); }]) .run(['$rootScope', function($rootScope) { $rootScope.records = [ - { id:1, title: "Miss Beulah Roob" }, - { id:2, title: "Trent Morissette" }, - { id:3, title: "Miss Ava Pouros" }, - { id:4, title: "Rod Pouros" }, - { id:5, title: "Abdul Rice" }, - { id:6, title: "Laurie Rutherford Sr." }, - { id:7, title: "Nakia McLaughlin" }, - { id:8, title: "Jordon Blanda DVM" }, - { id:9, title: "Rhoda Hand" }, - { id:10, title: "Alexandrea Sauer" } + { id: 1, title: 'Miss Beulah Roob' }, + { id: 2, title: 'Trent Morissette' }, + { id: 3, title: 'Miss Ava Pouros' }, + { id: 4, title: 'Rod Pouros' }, + { id: 5, title: 'Abdul Rice' }, + { id: 6, title: 'Laurie Rutherford Sr.' }, + { id: 7, title: 'Nakia McLaughlin' }, + { id: 8, title: 'Jordon Blanda DVM' }, + { id: 9, title: 'Rhoda Hand' }, + { id: 10, title: 'Alexandrea Sauer' } ]; }]) .controller('HomeController', [function() { //empty }]) - .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) { + .controller('ProfileController', ['$rootScope', '$routeParams', + function ProfileController($rootScope, $routeParams) { var index = parseInt($routeParams.id, 10); var record = $rootScope.records[index - 1]; this.title = record.title; this.id = record.id; @@ -3979,11 +4074,11 @@ </file> <file name="home.html"> <h2>Welcome to the home page</h1> <p>Please click on an element</p> <a class="record" - ng-href="#/profile/{{ record.id }}" + ng-href="#!/profile/{{ record.id }}" ng-animate-ref="{{ record.id }}" ng-repeat="record in records"> {{ record.title }} </a> </file> @@ -4055,11 +4150,11 @@ * Note that if the root element is on the `<html>` element then the cloned node will be placed inside of body. * * * ## Using $animate in your directive code * - * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application? + * So far we've explored how to feed in animations into an AngularJS application, but how do we trigger animations within our own directives in our application? * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's * imagine we have a greeting box that shows and hides itself when the data changes * * ```html * <greeting-box active="onOrOff">Hi there</greeting-box> @@ -4098,11 +4193,11 @@ * $animate.enter(element, parent).then(function() { * //the animation has completed * }); * ``` * - * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case + * (Note that earlier versions of AngularJS prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case * anymore.) * * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view * routing controller to hook into that: @@ -4113,29 +4208,59 @@ * // the animation for this route has completed * }]); * }]) * ``` * - * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.) + * (Note that you will need to trigger a digest within the callback to get AngularJS to notice any scope-related changes.) */ +var copy; +var extend; +var forEach; +var isArray; +var isDefined; +var isElement; +var isFunction; +var isObject; +var isString; +var isUndefined; +var jqLite; +var noop; + /** * @ngdoc service * @name $animate * @kind object * * @description * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. * * Click here {@link ng.$animate to learn more about animations with `$animate`}. */ -angular.module('ngAnimate', []) +angular.module('ngAnimate', [], function initAngularHelpers() { + // Access helpers from AngularJS core. + // Do it inside a `config` block to ensure `window.angular` is available. + noop = angular.noop; + copy = angular.copy; + extend = angular.extend; + jqLite = angular.element; + forEach = angular.forEach; + isArray = angular.isArray; + isString = angular.isString; + isObject = angular.isObject; + isUndefined = angular.isUndefined; + isDefined = angular.isDefined; + isFunction = angular.isFunction; + isElement = angular.isElement; +}) + .info({ angularVersion: '1.7.9' }) .directive('ngAnimateSwap', ngAnimateSwapDirective) .directive('ngAnimateChildren', $$AnimateChildrenDirective) .factory('$$rAFScheduler', $$rAFSchedulerFactory) .provider('$$animateQueue', $$AnimateQueueProvider) + .provider('$$animateCache', $$AnimateCacheProvider) .provider('$$animation', $$AnimationProvider) .provider('$animateCss', $AnimateCssProvider) .provider('$$animateCssDriver', $$AnimateCssDriverProvider)