- Use {@link ngAnimate.$animate $animate} to trigger animation operations within your directive code.
+ Use {@link ng.$animate $animate} to trigger animation operations within your directive code.
diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc
index 2af9cf23fd6a..14b62f1f9d1b 100644
--- a/docs/content/guide/animations.ngdoc
+++ b/docs/content/guide/animations.ngdoc
@@ -253,7 +253,7 @@ The table below explains in detail which animation events are triggered
| {@link ng.directive:ngClass#animations ngClass or {{class}}} | add and remove |
| {@link ng.directive:ngShow#animations ngShow & ngHide} | add and remove (the ng-hide class value) |
-For a full breakdown of the steps involved during each animation event, refer to the {@link ngAnimate.$animate API docs}.
+For a full breakdown of the steps involved during each animation event, refer to the {@link ng.$animate API docs}.
## How do I use animations in my own directives?
@@ -276,6 +276,6 @@ myModule.directive('my-directive', ['$animate', function($animate) {
## More about animations
-For a full breakdown of each method available on `$animate`, see the {@link ngAnimate.$animate API documentation}.
+For a full breakdown of each method available on `$animate`, see the {@link ng.$animate API documentation}.
To see a complete demo, see the {@link tutorial/step_12 animation step within the AngularJS phonecat tutorial}.
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index 9a7b182f669b..3f530626d3e2 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -57,6 +57,8 @@
$AnchorScrollProvider,
$AnimateProvider,
+ $$CoreAnimateQueueProvider,
+ $$CoreAnimateRunnerProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
@@ -217,6 +219,8 @@ function publishExternalAPI(angular) {
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
+ $$animateQueue: $$CoreAnimateQueueProvider,
+ $$AnimateRunner: $$CoreAnimateRunnerProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
diff --git a/src/loader.js b/src/loader.js
index 997a6c933d4b..02a2e2d66c43 100644
--- a/src/loader.js
+++ b/src/loader.js
@@ -218,7 +218,7 @@ function setupModuleLoader(window) {
*
*
* Defines an animation hook that can be later used with
- * {@link ngAnimate.$animate $animate} service and directives that use this service.
+ * {@link $animate $animate} service and directives that use this service.
*
* ```js
* module.animation('.animation-name', function($inject1, $inject2) {
diff --git a/src/ng/animate.js b/src/ng/animate.js
index f268773ef2af..449bbc6c34e7 100644
--- a/src/ng/animate.js
+++ b/src/ng/animate.js
@@ -1,6 +1,151 @@
'use strict';
var $animateMinErr = minErr('$animate');
+var ELEMENT_NODE = 1;
+
+function mergeClasses(a,b) {
+ if (!a && !b) return '';
+ if (!a) return b;
+ if (!b) return a;
+ if (isArray(a)) a = a.join(' ');
+ if (isArray(b)) b = b.join(' ');
+ return a + ' ' + b;
+}
+
+function extractElementNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ var elm = element[i];
+ if (elm.nodeType === ELEMENT_NODE) {
+ return elm;
+ }
+ }
+}
+
+function splitClasses(classes) {
+ if (isString(classes)) {
+ classes = classes.split(' ');
+ }
+
+ var obj = {};
+ forEach(classes, function(klass) {
+ // sometimes the split leaves empty string values
+ // incase extra spaces were applied to the options
+ if (klass.length) {
+ obj[klass] = true;
+ }
+ });
+ return obj;
+}
+
+var $$CoreAnimateRunnerProvider = function() {
+ this.$get = ['$q', '$$rAF', function($q, $$rAF) {
+ function AnimateRunner() {}
+ AnimateRunner.all = noop;
+ AnimateRunner.chain = noop;
+ AnimateRunner.prototype = {
+ end: noop,
+ cancel: noop,
+ resume: noop,
+ pause: noop,
+ complete: noop,
+ then: function(pass, fail) {
+ return $q(function(resolve) {
+ $$rAF(function() {
+ resolve();
+ });
+ }).then(pass, fail);
+ }
+ };
+ return AnimateRunner;
+ }];
+};
+
+// this is prefixed with Core since it conflicts with
+// the animateQueueProvider defined in ngAnimate/animateQueue.js
+var $$CoreAnimateQueueProvider = function() {
+ var postDigestQueue = new HashMap();
+ var postDigestElements = [];
+
+ this.$get = ['$$AnimateRunner', '$rootScope',
+ function($$AnimateRunner, $rootScope) {
+ return {
+ enabled: noop,
+ on: noop,
+ off: noop,
+
+ push: function(element, event, options, domOperation) {
+ domOperation && domOperation();
+
+ options = options || {};
+ options.from && element.css(options.from);
+ options.to && element.css(options.to);
+
+ if (options.addClass || options.removeClass) {
+ addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
+ }
+
+ return new $$AnimateRunner(); // jshint ignore:line
+ }
+ };
+
+ function addRemoveClassesPostDigest(element, add, remove) {
+ var data = postDigestQueue.get(element);
+ var classVal;
+
+ if (!data) {
+ postDigestQueue.put(element, data = {});
+ postDigestElements.push(element);
+ }
+
+ if (add) {
+ forEach(add.split(' '), function(className) {
+ if (className) {
+ data[className] = true;
+ }
+ });
+ }
+
+ if (remove) {
+ forEach(remove.split(' '), function(className) {
+ if (className) {
+ data[className] = false;
+ }
+ });
+ }
+
+ if (postDigestElements.length > 1) return;
+
+ $rootScope.$$postDigest(function() {
+ forEach(postDigestElements, function(element) {
+ var data = postDigestQueue.get(element);
+ if (data) {
+ var existing = splitClasses(element.attr('class'));
+ var toAdd = '';
+ var toRemove = '';
+ forEach(data, function(status, className) {
+ var hasClass = !!existing[className];
+ if (status !== hasClass) {
+ if (status) {
+ toAdd += (toAdd.length ? ' ' : '') + className;
+ } else {
+ toRemove += (toRemove.length ? ' ' : '') + className;
+ }
+ }
+ });
+
+ forEach(element, function(elm) {
+ toAdd && jqLiteAddClass(elm, toAdd);
+ toRemove && jqLiteRemoveClass(elm, toRemove);
+ });
+ postDigestQueue.remove(element);
+ }
+ });
+
+ postDigestElements.length = 0;
+ });
+ }
+ }];
+};
/**
* @ngdoc provider
@@ -8,20 +153,18 @@ var $animateMinErr = minErr('$animate');
*
* @description
* Default implementation of $animate that doesn't perform any animations, instead just
- * synchronously performs DOM
- * updates and calls done() callbacks.
+ * synchronously performs DOM updates and resolves the returned runner promise.
*
- * In order to enable animations the ngAnimate module has to be loaded.
+ * In order to enable animations the `ngAnimate` module has to be loaded.
*
- * To see the functional implementation check out src/ngAnimate/animate.js
+ * To see the functional implementation check out `src/ngAnimate/animate.js`.
*/
var $AnimateProvider = ['$provide', function($provide) {
+ var provider = this;
+ this.$$registeredAnimations = [];
- this.$$selectors = {};
-
-
- /**
+ /**
* @ngdoc method
* @name $animateProvider#register
*
@@ -30,33 +173,43 @@ var $AnimateProvider = ['$provide', function($provide) {
* animation object which contains callback functions for each event that is expected to be
* animated.
*
- * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction`
- * must be called once the element animation is complete. If a function is returned then the
- * animation service will use this function to cancel the animation whenever a cancel event is
- * triggered.
+ * * `eventFn`: `function(element, ... , doneFunction, options)`
+ * The element to animate, the `doneFunction` and the options fed into the animation. Depending
+ * on the type of animation additional arguments will be injected into the animation function. The
+ * list below explains the function signatures for the different animation methods:
*
+ * - setClass: function(element, addedClasses, removedClasses, doneFunction, options)
+ * - addClass: function(element, addedClasses, doneFunction, options)
+ * - removeClass: function(element, removedClasses, doneFunction, options)
+ * - enter, leave, move: function(element, doneFunction, options)
+ * - animate: function(element, fromStyles, toStyles, doneFunction, options)
+ *
+ * Make sure to trigger the `doneFunction` once the animation is fully complete.
*
* ```js
* return {
- * eventFn : function(element, done) {
- * //code to run the animation
- * //once complete, then run done()
- * return function cancellationFunction() {
- * //code to cancel the animation
- * }
- * }
- * }
+ * //enter, leave, move signature
+ * eventFn : function(element, done, options) {
+ * //code to run the animation
+ * //once complete, then run done()
+ * return function endFunction(wasCancelled) {
+ * //code to cancel the animation
+ * }
+ * }
+ * }
* ```
*
- * @param {string} name The name of the animation.
+ * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to).
* @param {Function} factory The factory function that will be executed to return the animation
* object.
*/
this.register = function(name, factory) {
+ if (name && name.charAt(0) !== '.') {
+ throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
+ }
+
var key = name + '-animation';
- if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel',
- "Expecting class selector starting with '.' got '{0}'.", name);
- this.$$selectors[name.substr(1)] = key;
+ provider.$$registeredAnimations[name.substr(1)] = key;
$provide.factory(key, factory);
};
@@ -67,8 +220,8 @@ var $AnimateProvider = ['$provide', function($provide) {
* @description
* Sets and/or returns the CSS class regular expression that is checked when performing
* an animation. Upon bootstrap the classNameFilter value is not set at all and will
- * therefore enable $animate to attempt to perform an animation on any element.
- * When setting the classNameFilter value, animations will only be performed on elements
+ * therefore enable $animate to attempt to perform an animation on any element that is triggered.
+ * When setting the `classNameFilter` value, animations will only be performed on elements
* that successfully match the filter expression. This in turn can boost performance
* for low-powered devices as well as applications containing a lot of structural operations.
* @param {RegExp=} expression The className expression which will be checked against all animations
@@ -81,98 +234,48 @@ var $AnimateProvider = ['$provide', function($provide) {
return this.$$classNameFilter;
};
- this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
-
- var currentDefer;
-
- function runAnimationPostDigest(fn) {
- var cancelFn, defer = $$q.defer();
- defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
- cancelFn && cancelFn();
- };
-
- $rootScope.$$postDigest(function ngAnimatePostDigest() {
- cancelFn = fn(function ngAnimateNotifyComplete() {
- defer.resolve();
- });
- });
-
- return defer.promise;
- }
-
- function resolveElementClasses(element, classes) {
- var toAdd = [], toRemove = [];
-
- var hasClasses = createMap();
- forEach((element.attr('class') || '').split(/\s+/), function(className) {
- hasClasses[className] = true;
- });
-
- forEach(classes, function(status, className) {
- var hasClass = hasClasses[className];
-
- // If the most recent class manipulation (via $animate) was to remove the class, and the
- // element currently has the class, the class is scheduled for removal. Otherwise, if
- // the most recent class manipulation (via $animate) was to add the class, and the
- // element does not currently have the class, the class is scheduled to be added.
- if (status === false && hasClass) {
- toRemove.push(className);
- } else if (status === true && !hasClass) {
- toAdd.push(className);
+ this.$get = ['$$animateQueue', function($$animateQueue) {
+ function domInsert(element, parentElement, afterElement) {
+ // if for some reason the previous element was removed
+ // from the dom sometime before this code runs then let's
+ // just stick to using the parent element as the anchor
+ if (afterElement) {
+ var afterNode = extractElementNode(afterElement);
+ if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) {
+ afterElement = null;
}
- });
-
- return (toAdd.length + toRemove.length) > 0 &&
- [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null];
- }
-
- function cachedClassManipulation(cache, classes, op) {
- for (var i=0, ii = classes.length; i < ii; ++i) {
- var className = classes[i];
- cache[className] = op;
- }
- }
-
- function asyncPromise() {
- // only serve one instance of a promise in order to save CPU cycles
- if (!currentDefer) {
- currentDefer = $$q.defer();
- $$asyncCallback(function() {
- currentDefer.resolve();
- currentDefer = null;
- });
- }
- return currentDefer.promise;
- }
-
- function applyStyles(element, options) {
- if (angular.isObject(options)) {
- var styles = extend(options.from || {}, options.to || {});
- element.css(styles);
}
+ afterElement ? afterElement.after(element) : parentElement.prepend(element);
}
/**
- *
* @ngdoc service
* @name $animate
- * @description The $animate service provides rudimentary DOM manipulation functions to
- * insert, remove and move elements within the DOM, as well as adding and removing classes.
- * This service is the core service used by the ngAnimate $animator service which provides
- * high-level animation hooks for CSS and JavaScript.
+ * @description The $animate service exposes a series of DOM utility methods that provide support
+ * for animation hooks. The default behavior is the application of DOM operations, however,
+ * when an animation is detected (and animations are enabled), $animate will do the heavy lifting
+ * to ensure that animation runs with the triggered DOM operation.
+ *
+ * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
+ * included and only when it is active then the animation hooks that `$animate` triggers will be
+ * functional. Once active then all structural `ng-` directives will trigger animations as they perform
+ * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
+ * `ngShow`, `ngHide` and `ngMessages` also provide support for animations.
*
- * $animate is available in the AngularJS core, however, the ngAnimate module must be included
- * to enable full out animation support. Otherwise, $animate will only perform simple DOM
- * manipulation operations.
+ * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives.
*
- * To learn more about enabling animation support, click here to visit the {@link ngAnimate
- * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service
- * page}.
+ * To learn more about enabling animation support, click here to visit the
+ * {@link ngAnimate ngAnimate module page}.
*/
return {
- animate: function(element, from, to) {
- applyStyles(element, { from: from, to: to });
- return asyncPromise();
+ // we don't call it directly since non-existant arguments may
+ // be interpreted as null within the sub enabled function
+ on: $$animateQueue.on,
+ off: $$animateQueue.off,
+ enabled: $$animateQueue.enabled,
+
+ cancel: function(runner) {
+ runner.cancel && runner.end();
},
/**
@@ -180,193 +283,172 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#enter
* @kind function
- * @description Inserts the element into the DOM either after the `after` element or
- * as the first child within the `parent` element. When the function is called a promise
- * is returned that will be resolved at a later time.
+ * @description Inserts the element into the DOM either after the `after` element (if provided) or
+ * as the first child within the `parent` element and then triggers an animation.
+ * A promise is returned that will be resolved during the next digest once the animation
+ * has completed.
+ *
* @param {DOMElement} element the element which will be inserted into the DOM
* @param {DOMElement} parent the parent element which will append the element as
- * a child (if the after element is not present)
- * @param {DOMElement} after the sibling element which will append the element
- * after itself
- * @param {object=} options an optional collection of styles that will be applied to the element.
+ * a child (so long as the after element is not present)
+ * @param {DOMElement=} after the sibling element after which the element will be appended
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
enter: function(element, parent, after, options) {
- applyStyles(element, options);
- after ? after.after(element)
- : parent.prepend(element);
- return asyncPromise();
+ parent = parent || after.parent();
+ domInsert(element, parent, after);
+ return $$animateQueue.push(element, 'enter', options);
},
/**
*
* @ngdoc method
- * @name $animate#leave
+ * @name $animate#move
* @kind function
- * @description Removes the element from the DOM. When the function is called a promise
- * is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will be removed from the DOM
- * @param {object=} options an optional collection of options that will be applied to the element.
+ * @description Inserts (moves) the element into its new position in the DOM either after
+ * the `after` element (if provided) or as the first child within the `parent` element
+ * and then triggers an animation. A promise is returned that will be resolved
+ * during the next digest once the animation has completed.
+ *
+ * @param {DOMElement} element the element which will be moved into the new DOM position
+ * @param {DOMElement} parent the parent element which will append the element as
+ * a child (so long as the after element is not present)
+ * @param {DOMElement=} after the sibling element after which the element will be appended
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
- leave: function(element, options) {
- applyStyles(element, options);
- element.remove();
- return asyncPromise();
+ move: function(element, parent, after, options) {
+ parent = parent || after.parent();
+ domInsert(element, parent, after);
+ return $$animateQueue.push(element, 'move', options);
},
/**
- *
* @ngdoc method
- * @name $animate#move
+ * @name $animate#leave
* @kind function
- * @description Moves the position of the provided element within the DOM to be placed
- * either after the `after` element or inside of the `parent` element. When the function
- * is called a promise is returned that will be resolved at a later time.
+ * @description Triggers an animation and then removes the element from the DOM.
+ * When the function is called a promise is returned that will be resolved during the next
+ * digest once the animation has completed.
+ *
+ * @param {DOMElement} element the element which will be removed from the DOM
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
*
- * @param {DOMElement} element the element which will be moved around within the
- * DOM
- * @param {DOMElement} parent the parent element where the element will be
- * inserted into (if the after element is not present)
- * @param {DOMElement} after the sibling element where the element will be
- * positioned next to
- * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- move: function(element, parent, after, options) {
- // Do not remove element before insert. Removing will cause data associated with the
- // element to be dropped. Insert will implicitly do the remove.
- return this.enter(element, parent, after, options);
+ leave: function(element, options) {
+ return $$animateQueue.push(element, 'leave', options, function() {
+ element.remove();
+ });
},
/**
- *
* @ngdoc method
* @name $animate#addClass
* @kind function
- * @description Adds the provided className CSS class value to the provided element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have the className value
- * added to it
- * @param {string} className the CSS class which will be added to the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon
+ * execution, the addClass operation will only be handled after the next digest and it will not trigger an
+ * animation if element already contains the CSS class or if the class is removed at a later step.
+ * Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
addClass: function(element, className, options) {
- return this.setClass(element, className, [], options);
- },
-
- $$addClassImmediately: function(element, className, options) {
- element = jqLite(element);
- className = !isString(className)
- ? (isArray(className) ? className.join(' ') : '')
- : className;
- forEach(element, function(element) {
- jqLiteAddClass(element, className);
- });
- applyStyles(element, options);
- return asyncPromise();
+ options = options || {};
+ options.addClass = mergeClasses(options.addclass, className);
+ return $$animateQueue.push(element, 'addClass', options);
},
/**
- *
* @ngdoc method
* @name $animate#removeClass
* @kind function
- * @description Removes the provided className CSS class value from the provided element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have the className value
- * removed from it
- * @param {string} className the CSS class which will be removed from the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon
+ * execution, the removeClass operation will only be handled after the next digest and it will not trigger an
+ * animation if element does not contain the CSS class or if the class is added at a later step.
+ * Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
removeClass: function(element, className, options) {
- return this.setClass(element, [], className, options);
- },
-
- $$removeClassImmediately: function(element, className, options) {
- element = jqLite(element);
- className = !isString(className)
- ? (isArray(className) ? className.join(' ') : '')
- : className;
- forEach(element, function(element) {
- jqLiteRemoveClass(element, className);
- });
- applyStyles(element, options);
- return asyncPromise();
+ options = options || {};
+ options.removeClass = mergeClasses(options.removeClass, className);
+ return $$animateQueue.push(element, 'removeClass', options);
},
/**
- *
* @ngdoc method
* @name $animate#setClass
* @kind function
- * @description Adds and/or removes the given CSS classes to and from the element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have its CSS classes changed
- * removed from it
- * @param {string} add the CSS classes which will be added to the element
- * @param {string} remove the CSS class which will be removed from the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Performs both the addition and removal of a CSS classes on an element and (during the process)
+ * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and
+ * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has
+ * passed. Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces)
+ * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
setClass: function(element, add, remove, options) {
- var self = this;
- var STORAGE_KEY = '$$animateClasses';
- var createdCache = false;
- element = jqLite(element);
-
- var cache = element.data(STORAGE_KEY);
- if (!cache) {
- cache = {
- classes: {},
- options: options
- };
- createdCache = true;
- } else if (options && cache.options) {
- cache.options = angular.extend(cache.options || {}, options);
- }
-
- var classes = cache.classes;
-
- add = isArray(add) ? add : add.split(' ');
- remove = isArray(remove) ? remove : remove.split(' ');
- cachedClassManipulation(classes, add, true);
- cachedClassManipulation(classes, remove, false);
-
- if (createdCache) {
- cache.promise = runAnimationPostDigest(function(done) {
- var cache = element.data(STORAGE_KEY);
- element.removeData(STORAGE_KEY);
-
- // in the event that the element is removed before postDigest
- // is run then the cache will be undefined and there will be
- // no need anymore to add or remove and of the element classes
- if (cache) {
- var classes = resolveElementClasses(element, cache.classes);
- if (classes) {
- self.$$setClassImmediately(element, classes[0], classes[1], cache.options);
- }
- }
-
- done();
- });
- element.data(STORAGE_KEY, cache);
- }
-
- return cache.promise;
- },
-
- $$setClassImmediately: function(element, add, remove, options) {
- add && this.$$addClassImmediately(element, add);
- remove && this.$$removeClassImmediately(element, remove);
- applyStyles(element, options);
- return asyncPromise();
+ options = options || {};
+ options.addClass = mergeClasses(options.addClass, add);
+ options.removeClass = mergeClasses(options.removeClass, remove);
+ return $$animateQueue.push(element, 'setClass', options);
},
- enabled: noop,
- cancel: noop
+ /**
+ * @ngdoc method
+ * @name $animate#animate
+ * @kind function
+ *
+ * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
+ * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take
+ * on the provided styles. For example, if a transition animation is set for the given className then the provided from and
+ * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles
+ * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter).
+ *
+ * @param {DOMElement} element the element which the CSS styles will be applied to
+ * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation.
+ * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation.
+ * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If
+ * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element.
+ * (Note that if no animation is detected then this value will not be appplied to the element.)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
+ * @return {Promise} the animation callback promise
+ */
+ animate: function(element, from, to, className, options) {
+ options = options || {};
+ options.from = options.from ? extend(options.from, from) : from;
+ options.to = options.to ? extend(options.to, to) : to;
+
+ className = className || 'ng-inline-animate';
+ options.tempClasses = mergeClasses(options.tempClasses, className);
+ return $$animateQueue.push(element, 'animate', options);
+ }
};
}];
}];
diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js
index 3616c5ebf072..af8d07388cd7 100644
--- a/src/ng/directive/ngClass.js
+++ b/src/ng/directive/ngClass.js
@@ -275,8 +275,8 @@ function classDirective(name, selector) {
The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
- to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and
- {@link ng.$animate#removeClass $animate.removeClass}.
+ to view the step by step details of {@link $animate#addClass $animate.addClass} and
+ {@link $animate#removeClass $animate.removeClass}.
*/
var ngClassDirective = classDirective('', true);
diff --git a/src/ngAnimate/.jshintrc b/src/ngAnimate/.jshintrc
index b738582198ed..c6a112ff0f22 100644
--- a/src/ngAnimate/.jshintrc
+++ b/src/ngAnimate/.jshintrc
@@ -1,8 +1,40 @@
{
"extends": "../../.jshintrc-base",
"maxlen": false, /* ngAnimate docs contain wide tables */
+ "newcap": false,
"browser": true,
"globals": {
- "angular": false
+ "angular": false,
+ "noop": false,
+
+ "forEach": false,
+ "extend": false,
+ "jqLite": false,
+ "forEach": false,
+ "isArray": false,
+ "isString": false,
+ "isObject": false,
+ "isUndefined": false,
+ "isDefined": false,
+ "isFunction": false,
+ "isElement": false,
+
+ "ELEMENT_NODE": false,
+ "NG_ANIMATE_CHILDREN_DATA": false,
+
+ "isPromiseLike": false,
+ "mergeClasses": false,
+ "mergeAnimationOptions": false,
+ "prepareAnimationOptions": false,
+ "applyAnimationStyles": false,
+ "applyAnimationFromStyles": false,
+ "applyAnimationToStyles": false,
+ "applyAnimationClassesFactory": false,
+ "pendClasses": false,
+ "normalizeCssProp": false,
+ "packageStyles": false,
+ "removeFromArray": false,
+ "stripCommentsFromElement": false,
+ "extractElementNode": false
}
-}
\ No newline at end of file
+}
diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js
deleted file mode 100644
index b87ed335cb2b..000000000000
--- a/src/ngAnimate/animate.js
+++ /dev/null
@@ -1,2130 +0,0 @@
-'use strict';
-/* jshint maxlen: false */
-
-/**
- * @ngdoc module
- * @name ngAnimate
- * @description
- *
- * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives.
- *
- *
- *
- * # Usage
- *
- * To see animations in action, all that is required is to define the appropriate CSS classes
- * or to register a JavaScript animation via the `myModule.animation()` function. The directives that support animation automatically are:
- * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation
- * by using the `$animate` service.
- *
- * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives:
- *
- * | Directive | Supported Animations |
- * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
- * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
- * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
- * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
- * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
- * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
- * | {@link ng.directive: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 |
- *
- * You can find out more information about animations upon visiting each directive page.
- *
- * Below is an example of how to apply animations to a directive that supports animation hooks:
- *
- * ```html
- *
- *
- *
- *
- * ```
- *
- * Keep in mind that, by default, if an animation is running, any child elements cannot be animated
- * until the parent element's animation has completed. This blocking feature can be overridden by
- * placing the `ng-animate-children` attribute on a parent container tag.
- *
- * ```html
- *
- *
- *
- * ...
- *
- *
- *
- * ```
- *
- * When the `on` expression value changes and an animation is triggered then each of the elements within
- * will all animate without the block being applied to child elements.
- *
- * ## Are animations run when the application starts?
- * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid
- * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work,
- * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering
- * layout changes in the application will trigger animations as normal.
- *
- * In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular
- * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
- * are complete.
- *
- * ## CSS-defined Animations
- * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
- * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
- * and can be used to play along with this naming structure.
- *
- * The following code below demonstrates how to perform animations using **CSS transitions** with Angular:
- *
- * ```html
- *
- *
- *
- *
- *
- * ```
- *
- * The following code below demonstrates how to perform animations using **CSS animations** with Angular:
- *
- * ```html
- *
- *
- *
- *
- *
- * ```
- *
- * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing.
- *
- * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add
- * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically
- * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be
- * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end
- * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element
- * has no CSS transition/animation classes applied to it.
- *
- * ### Structural transition animations
- *
- * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition
- * value to force the browser into rendering the styles defined in the setup (`.ng-enter`, `.ng-leave`
- * or `.ng-move`) class. This means that any active transition animations operating on the element
- * will be cut off to make way for the enter, leave or move animation.
- *
- * ### Class-based transition animations
- *
- * Class-based transitions refer to transition animations that are triggered when a CSS class is
- * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`,
- * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`).
- * They are different when compared to structural animations since they **do not cancel existing
- * animations** nor do they **block successive transitions** from rendering on the same element.
- * This distinction allows for **multiple class-based transitions** to be performed on the same element.
- *
- * In addition to ngAnimate supporting the default (natural) functionality of class-based transition
- * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the
- * developer in further styling the element throughout the transition animation. Earlier versions
- * of ngAnimate may have caused natural CSS transitions to break and not render properly due to
- * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class
- * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of
- * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS
- * class transitions are compatible with ngAnimate.
- *
- * There is, however, one special case when dealing with class-based transitions in ngAnimate.
- * When rendering class-based transitions that make use of the setup and active CSS classes
- * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define
- * the transition value **on the active CSS class** and not the setup class.
- *
- * ```css
- * .fade-add {
- * /* remember to place a 0s transition here
- * to ensure that the styles are applied instantly
- * even if the element already has a transition style */
- * transition:0s linear all;
- *
- * /* starting CSS styles */
- * opacity:1;
- * }
- * .fade-add.fade-add-active {
- * /* this will be the length of the animation */
- * transition:1s linear all;
- * opacity:0;
- * }
- * ```
- *
- * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it
- * has a duration of zero. This may not be required, however, incase the browser is unable to render
- * the styling present in this CSS class instantly then it could be that the browser is attempting
- * to perform an unnecessary transition.
- *
- * This workaround, however, does not apply to standard class-based transitions that are rendered
- * when a CSS class containing a transition is applied to an element:
- *
- * ```css
- * /* this works as expected */
- * .fade {
- * transition:1s linear all;
- * opacity:0;
- * }
- * ```
- *
- * Please keep this in mind when coding the CSS markup that will be used within class-based transitions.
- * Also, try not to mix the two class-based animation flavors together since the CSS code may become
- * overly complex.
- *
- *
- * ### Preventing Collisions With Third Party Libraries
- *
- * Some third-party frameworks place animation duration defaults across many element or className
- * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which
- * is expecting actual animations on these elements and has to wait for their completion.
- *
- * You can prevent this unwanted behavior by using a prefix on all your animation classes:
- *
- * ```css
- * /* prefixed with animate- */
- * .animate-fade-add.animate-fade-add-active {
- * transition:1s linear all;
- * opacity:0;
- * }
- * ```
- *
- * You then configure `$animate` to enforce this prefix:
- *
- * ```js
- * $animateProvider.classNameFilter(/animate-/);
- * ```
- *
- *
- * ### CSS Staggering Animations
- * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
- * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
- * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
- * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
- * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
- *
- * ```css
- * .my-animation.ng-enter {
- * /* standard transition code */
- * -webkit-transition: 1s linear all;
- * transition: 1s linear all;
- * opacity:0;
- * }
- * .my-animation.ng-enter-stagger {
- * /* this will have a 100ms delay between each successive leave animation */
- * -webkit-transition-delay: 0.1s;
- * transition-delay: 0.1s;
- *
- * /* in case the stagger doesn't work then these two values
- * must be set to 0 to avoid an accidental CSS inheritance */
- * -webkit-transition-duration: 0s;
- * transition-duration: 0s;
- * }
- * .my-animation.ng-enter.ng-enter-active {
- * /* standard transition styles */
- * opacity:1;
- * }
- * ```
- *
- * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
- * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
- * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
- * will also be reset if more than 10ms has passed after the last animation has been fired.
- *
- * The following code will issue the **ng-leave-stagger** event on the element provided:
- *
- * ```js
- * var kids = parent.children();
- *
- * $animate.leave(kids[0]); //stagger index=0
- * $animate.leave(kids[1]); //stagger index=1
- * $animate.leave(kids[2]); //stagger index=2
- * $animate.leave(kids[3]); //stagger index=3
- * $animate.leave(kids[4]); //stagger index=4
- *
- * $timeout(function() {
- * //stagger has reset itself
- * $animate.leave(kids[5]); //stagger index=0
- * $animate.leave(kids[6]); //stagger index=1
- * }, 100, false);
- * ```
- *
- * Stagger animations are currently only supported within CSS-defined animations.
- *
- * ## JavaScript-defined Animations
- * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not
- * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module.
- *
- * ```js
- * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application.
- * var ngModule = angular.module('YourApp', ['ngAnimate']);
- * ngModule.animation('.my-crazy-animation', function() {
- * return {
- * enter: function(element, done) {
- * //run the animation here and call done when the animation is complete
- * return function(cancelled) {
- * //this (optional) function will be called when the animation
- * //completes or when the animation is cancelled (the cancelled
- * //flag will be set to true if cancelled).
- * };
- * },
- * leave: function(element, done) { },
- * move: function(element, done) { },
- *
- * //animation that can be triggered before the class is added
- * beforeAddClass: function(element, className, done) { },
- *
- * //animation that can be triggered after the class is added
- * addClass: function(element, className, done) { },
- *
- * //animation that can be triggered before the class is removed
- * beforeRemoveClass: function(element, className, done) { },
- *
- * //animation that can be triggered after the class is removed
- * removeClass: function(element, className, done) { }
- * };
- * });
- * ```
- *
- * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run
- * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits
- * the element's CSS class attribute value and then run the matching animation event function (if found).
- * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will
- * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported).
- *
- * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned.
- * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run,
- * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation
- * or transition code that is defined via a stylesheet).
- *
- *
- * ### Applying Directive-specific Styles to an Animation
- * In some cases a directive or service may want to provide `$animate` with extra details that the animation will
- * include into its animation. Let's say for example we wanted to render an animation that animates an element
- * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click
- * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function
- * call to `$animate.addClass`.
- *
- * ```js
- * canvas.on('click', function(e) {
- * $animate.addClass(element, 'on', {
- * to: {
- * left : e.client.x + 'px',
- * top : e.client.y + 'px'
- * }
- * }):
- * });
- * ```
- *
- * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will
- * also include and transition the styling of the `left` and `top` properties into its running animation. If we want
- * to provide some starting animation values then we can do so by placing the starting animations styles into an object
- * called `from` in the same object as the `to` animations.
- *
- * ```js
- * canvas.on('click', function(e) {
- * $animate.addClass(element, 'on', {
- * from: {
- * position: 'absolute',
- * left: '0px',
- * top: '0px'
- * },
- * to: {
- * left : e.client.x + 'px',
- * top : e.client.y + 'px'
- * }
- * }):
- * });
- * ```
- *
- * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the
- * element. If `ngAnimate` is not present then the styles will be applied immediately.
- *
- */
-
-angular.module('ngAnimate', ['ng'])
-
- /**
- * @ngdoc provider
- * @name $animateProvider
- * @description
- *
- * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module.
- * When an animation is triggered, the $animate service will query the $animate service to find any animations that match
- * the provided name value.
- *
- * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
- *
- * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
- *
- */
- .directive('ngAnimateChildren', function() {
- var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
- return function(scope, element, attrs) {
- var val = attrs.ngAnimateChildren;
- if (angular.isString(val) && val.length === 0) { //empty attribute
- element.data(NG_ANIMATE_CHILDREN, true);
- } else {
- scope.$watch(val, function(value) {
- element.data(NG_ANIMATE_CHILDREN, !!value);
- });
- }
- };
- })
-
- //this private service is only used within CSS-enabled animations
- //IE8 + IE9 do not support rAF natively, but that is fine since they
- //also don't support transitions and keyframes which means that the code
- //below will never be used by the two browsers.
- .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) {
- var bod = $document[0].body;
- return function(fn) {
- //the returned function acts as the cancellation function
- return $$rAF(function() {
- //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 perform multi-class CSS based animations with
- //Firefox. 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!
- fn(bod.offsetWidth);
- });
- };
- }])
-
- .config(['$provide', '$animateProvider', function($provide, $animateProvider) {
- var noop = angular.noop;
- var forEach = angular.forEach;
- var selectors = $animateProvider.$$selectors;
- var isArray = angular.isArray;
- var isString = angular.isString;
- var isObject = angular.isObject;
-
- var ELEMENT_NODE = 1;
- var NG_ANIMATE_STATE = '$$ngAnimateState';
- var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren';
- var NG_ANIMATE_CLASS_NAME = 'ng-animate';
- var rootAnimateState = {running: true};
-
- function extractElementNode(element) {
- for (var i = 0; i < element.length; i++) {
- var elm = element[i];
- if (elm.nodeType == ELEMENT_NODE) {
- return elm;
- }
- }
- }
-
- function prepareElement(element) {
- return element && angular.element(element);
- }
-
- function stripCommentsFromElement(element) {
- return angular.element(extractElementNode(element));
- }
-
- function isMatchingElement(elm1, elm2) {
- return extractElementNode(elm1) == extractElementNode(elm2);
- }
- var $$jqLite;
- $provide.decorator('$animate',
- ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', '$$jqLite',
- function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest, $$$jqLite) {
-
- $$jqLite = $$$jqLite;
- $rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
-
- // 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
- // templates currently downloading then the watcher will still fire anyway.
- var deregisterWatch = $rootScope.$watch(
- function() { return $templateRequest.totalPendingRequests; },
- function(val, oldVal) {
- if (val !== 0) return;
- deregisterWatch();
-
- // Now that all templates have been downloaded, $animate will wait until
- // the post digest queue is empty before enabling animations. By having two
- // calls to $postDigest calls we can ensure that the flag is enabled at the
- // very end of the post digest queue. Since all of the animations in $animate
- // use $postDigest, it's important that the code below executes at the end.
- // This basically means that the page is fully downloaded and compiled before
- // any animations are triggered.
- $rootScope.$$postDigest(function() {
- $rootScope.$$postDigest(function() {
- rootAnimateState.running = false;
- });
- });
- }
- );
-
- var globalAnimationCounter = 0;
- var classNameFilter = $animateProvider.classNameFilter();
- var isAnimatableClassName = !classNameFilter
- ? function() { return true; }
- : function(className) {
- return classNameFilter.test(className);
- };
-
- function classBasedAnimationsBlocked(element, setter) {
- var data = element.data(NG_ANIMATE_STATE) || {};
- if (setter) {
- data.running = true;
- data.structural = true;
- element.data(NG_ANIMATE_STATE, data);
- }
- return data.disabled || (data.running && data.structural);
- }
-
- function runAnimationPostDigest(fn) {
- var cancelFn, defer = $$q.defer();
- defer.promise.$$cancelFn = function() {
- cancelFn && cancelFn();
- };
- $rootScope.$$postDigest(function() {
- cancelFn = fn(function() {
- defer.resolve();
- });
- });
- return defer.promise;
- }
-
- function parseAnimateOptions(options) {
- // some plugin code may still be passing in the callback
- // function as the last param for the $animate methods so
- // it's best to only allow string or array values for now
- if (isObject(options)) {
- if (options.tempClasses && isString(options.tempClasses)) {
- options.tempClasses = options.tempClasses.split(/\s+/);
- }
- return options;
- }
- }
-
- function resolveElementClasses(element, cache, runningAnimations) {
- runningAnimations = runningAnimations || {};
-
- var lookup = {};
- forEach(runningAnimations, function(data, selector) {
- forEach(selector.split(' '), function(s) {
- lookup[s]=data;
- });
- });
-
- var hasClasses = Object.create(null);
- forEach((element.attr('class') || '').split(/\s+/), function(className) {
- hasClasses[className] = true;
- });
-
- var toAdd = [], toRemove = [];
- forEach((cache && cache.classes) || [], function(status, className) {
- var hasClass = hasClasses[className];
- var matchingAnimation = lookup[className] || {};
-
- // When addClass and removeClass is called then $animate will check to
- // see if addClass and removeClass cancel each other out. When there are
- // more calls to removeClass than addClass then the count falls below 0
- // and then the removeClass animation will be allowed. Otherwise if the
- // count is above 0 then that means an addClass animation will commence.
- // Once an animation is allowed then the code will also check to see if
- // there exists any on-going animation that is already adding or remvoing
- // the matching CSS class.
- if (status === false) {
- //does it have the class or will it have the class
- if (hasClass || matchingAnimation.event == 'addClass') {
- toRemove.push(className);
- }
- } else if (status === true) {
- //is the class missing or will it be removed?
- if (!hasClass || matchingAnimation.event == 'removeClass') {
- toAdd.push(className);
- }
- }
- });
-
- return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
- }
-
- function lookup(name) {
- if (name) {
- var matches = [],
- flagMap = {},
- classes = name.substr(1).split('.');
-
- //the empty string value is the default animation
- //operation which performs CSS transition and keyframe
- //animations sniffing. This is always included for each
- //element animation procedure if the browser supports
- //transitions and/or keyframe animations. The default
- //animation is added to the top of the list to prevent
- //any previous animations from affecting the element styling
- //prior to the element being animated.
- if ($sniffer.transitions || $sniffer.animations) {
- matches.push($injector.get(selectors['']));
- }
-
- for (var i=0; i < classes.length; i++) {
- var klass = classes[i],
- selectorFactoryName = selectors[klass];
- if (selectorFactoryName && !flagMap[klass]) {
- matches.push($injector.get(selectorFactoryName));
- flagMap[klass] = true;
- }
- }
- return matches;
- }
- }
-
- function animationRunner(element, animationEvent, className, options) {
- //transcluded directives may sometimes fire an animation using only comment nodes
- //best to catch this early on to prevent any animation operations from occurring
- var node = element[0];
- if (!node) {
- return;
- }
-
- if (options) {
- options.to = options.to || {};
- options.from = options.from || {};
- }
-
- var classNameAdd;
- var classNameRemove;
- if (isArray(className)) {
- classNameAdd = className[0];
- classNameRemove = className[1];
- if (!classNameAdd) {
- className = classNameRemove;
- animationEvent = 'removeClass';
- } else if (!classNameRemove) {
- className = classNameAdd;
- animationEvent = 'addClass';
- } else {
- className = classNameAdd + ' ' + classNameRemove;
- }
- }
-
- var isSetClassOperation = animationEvent == 'setClass';
- var isClassBased = isSetClassOperation
- || animationEvent == 'addClass'
- || animationEvent == 'removeClass'
- || animationEvent == 'animate';
-
- var currentClassName = element.attr('class');
- var classes = currentClassName + ' ' + className;
- if (!isAnimatableClassName(classes)) {
- return;
- }
-
- var beforeComplete = noop,
- beforeCancel = [],
- before = [],
- afterComplete = noop,
- afterCancel = [],
- after = [];
-
- var animationLookup = (' ' + classes).replace(/\s+/g,'.');
- forEach(lookup(animationLookup), function(animationFactory) {
- var created = registerAnimation(animationFactory, animationEvent);
- if (!created && isSetClassOperation) {
- registerAnimation(animationFactory, 'addClass');
- registerAnimation(animationFactory, 'removeClass');
- }
- });
-
- function registerAnimation(animationFactory, event) {
- var afterFn = animationFactory[event];
- var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)];
- if (afterFn || beforeFn) {
- if (event == 'leave') {
- beforeFn = afterFn;
- //when set as null then animation knows to skip this phase
- afterFn = null;
- }
- after.push({
- event: event, fn: afterFn
- });
- before.push({
- event: event, fn: beforeFn
- });
- return true;
- }
- }
-
- function run(fns, cancellations, allCompleteFn) {
- var animations = [];
- forEach(fns, function(animation) {
- animation.fn && animations.push(animation);
- });
-
- var count = 0;
- function afterAnimationComplete(index) {
- if (cancellations) {
- (cancellations[index] || noop)();
- if (++count < animations.length) return;
- cancellations = null;
- }
- allCompleteFn();
- }
-
- //The code below adds directly to the array in order to work with
- //both sync and async animations. Sync animations are when the done()
- //operation is called right away. DO NOT REFACTOR!
- forEach(animations, function(animation, index) {
- var progress = function() {
- afterAnimationComplete(index);
- };
- switch (animation.event) {
- case 'setClass':
- cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options));
- break;
- case 'animate':
- cancellations.push(animation.fn(element, className, options.from, options.to, progress));
- break;
- case 'addClass':
- cancellations.push(animation.fn(element, classNameAdd || className, progress, options));
- break;
- case 'removeClass':
- cancellations.push(animation.fn(element, classNameRemove || className, progress, options));
- break;
- default:
- cancellations.push(animation.fn(element, progress, options));
- break;
- }
- });
-
- if (cancellations && cancellations.length === 0) {
- allCompleteFn();
- }
- }
-
- return {
- node: node,
- event: animationEvent,
- className: className,
- isClassBased: isClassBased,
- isSetClassOperation: isSetClassOperation,
- applyStyles: function() {
- if (options) {
- element.css(angular.extend(options.from || {}, options.to || {}));
- }
- },
- before: function(allCompleteFn) {
- beforeComplete = allCompleteFn;
- run(before, beforeCancel, function() {
- beforeComplete = noop;
- allCompleteFn();
- });
- },
- after: function(allCompleteFn) {
- afterComplete = allCompleteFn;
- run(after, afterCancel, function() {
- afterComplete = noop;
- allCompleteFn();
- });
- },
- cancel: function() {
- if (beforeCancel) {
- forEach(beforeCancel, function(cancelFn) {
- (cancelFn || noop)(true);
- });
- beforeComplete(true);
- }
- if (afterCancel) {
- forEach(afterCancel, function(cancelFn) {
- (cancelFn || noop)(true);
- });
- afterComplete(true);
- }
- }
- };
- }
-
- /**
- * @ngdoc service
- * @name $animate
- * @kind object
- *
- * @description
- * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations.
- * When any of these operations are run, the $animate service
- * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object)
- * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run.
- *
- * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives
- * will work out of the box without any extra configuration.
- *
- * Requires the {@link ngAnimate `ngAnimate`} module to be installed.
- *
- * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application.
- * ## Callback Promises
- * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The
- * promise itself is then resolved once the animation has completed itself, has been cancelled or has been
- * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still
- * call the resolve function of the animation.)
- *
- * ```js
- * $animate.enter(element, container).then(function() {
- * //...this is called once the animation is complete...
- * });
- * ```
- *
- * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope,
- * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using
- * `$scope.$apply(...)`;
- *
- * ```js
- * $animate.leave(element).then(function() {
- * $scope.$apply(function() {
- * $location.path('/new-page');
- * });
- * });
- * ```
- *
- * An animation can also be cancelled by calling the `$animate.cancel(promise)` method with the provided
- * promise that was returned when the animation was started.
- *
- * ```js
- * var promise = $animate.addClass(element, 'super-long-animation');
- * promise.then(function() {
- * //this will still be called even if cancelled
- * });
- *
- * element.on('click', function() {
- * //tooo lazy to wait for the animation to end
- * $animate.cancel(promise);
- * });
- * ```
- *
- * (Keep in mind that the promise cancellation is unique to `$animate` since promises in
- * general cannot be cancelled.)
- *
- */
- return {
- /**
- * @ngdoc method
- * @name $animate#animate
- * @kind function
- *
- * @description
- * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element.
- * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation
- * will take on the provided styles. For example, if a transition animation is set for the given className then the
- * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is
- * detected then the provided styles will be given in as function paramters.
- *
- * ```js
- * ngModule.animation('.my-inline-animation', function() {
- * return {
- * animate : function(element, className, from, to, done) {
- * //styles
- * }
- * }
- * });
- * ```
- *
- * Below is a breakdown of each step that occurs during the `animate` animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|
- * | 1. `$animate.animate(...)` is called | `class="my-animation"` |
- * | 2. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
- * | 3. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
- * | 4. the `className` class value is added to the element | `class="my-animation ng-animate className"` |
- * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate className"` |
- * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.className` class styling is applied right away| `class="my-animation ng-animate className"` |
- * | 7. `$animate` applies the provided collection of `from` CSS styles to the element | `class="my-animation ng-animate className"` |
- * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate className"` |
- * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate className"` |
- * | 10. the `className-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate className className-active"` |
- * | 11. `$animate` applies the collection of `to` CSS styles to the element which are then handled by the transition | `class="my-animation ng-animate className className-active"` |
- * | 12. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate className className-active"` |
- * | 13. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
- * | 14. The returned promise is resolved. | `class="my-animation"` |
- *
- * @param {DOMElement} element the element that will be the focus of the enter animation
- * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation
- * @param {object} to a collection of CSS styles that the element will animate towards
- * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`)
- * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- animate: function(element, from, to, className, options) {
- className = className || 'ng-inline-animate';
- options = parseAnimateOptions(options) || {};
- options.from = to ? from : null;
- options.to = to ? to : from;
-
- return runAnimationPostDigest(function(done) {
- return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done);
- });
- },
-
- /**
- * @ngdoc method
- * @name $animate#enter
- * @kind function
- *
- * @description
- * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once
- * the animation is started, the following CSS classes will be present on the element for the duration of the animation:
- *
- * Below is a breakdown of each step that occurs during enter animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
- * | 1. `$animate.enter(...)` is called | `class="my-animation"` |
- * | 2. element is inserted into the `parentElement` element or beside the `afterElement` element | `class="my-animation"` |
- * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
- * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
- * | 5. the `.ng-enter` class is added to the element | `class="my-animation ng-animate ng-enter"` |
- * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-enter"` |
- * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-enter` class styling is applied right away | `class="my-animation ng-animate ng-enter"` |
- * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-enter"` |
- * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-enter"` |
- * | 10. the `.ng-enter-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
- * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-enter ng-enter-active"` |
- * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
- * | 13. The returned promise is resolved. | `class="my-animation"` |
- *
- * @param {DOMElement} element the element that will be the focus of the enter animation
- * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation
- * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation
- * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- enter: function(element, parentElement, afterElement, options) {
- options = parseAnimateOptions(options);
- element = angular.element(element);
- parentElement = prepareElement(parentElement);
- afterElement = prepareElement(afterElement);
-
- classBasedAnimationsBlocked(element, true);
- $delegate.enter(element, parentElement, afterElement);
- return runAnimationPostDigest(function(done) {
- return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
- });
- },
-
- /**
- * @ngdoc method
- * @name $animate#leave
- * @kind function
- *
- * @description
- * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once
- * the animation is started, the following CSS classes will be added for the duration of the animation:
- *
- * Below is a breakdown of each step that occurs during leave animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|
- * | 1. `$animate.leave(...)` is called | `class="my-animation"` |
- * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
- * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
- * | 4. the `.ng-leave` class is added to the element | `class="my-animation ng-animate ng-leave"` |
- * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-leave"` |
- * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.ng-leave` class styling is applied right away | `class="my-animation ng-animate ng-leave"` |
- * | 7. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-leave"` |
- * | 8. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-leave"` |
- * | 9. the `.ng-leave-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
- * | 10. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-leave ng-leave-active"` |
- * | 11. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
- * | 12. The element is removed from the DOM | ... |
- * | 13. The returned promise is resolved. | ... |
- *
- * @param {DOMElement} element the element that will be the focus of the leave animation
- * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- leave: function(element, options) {
- options = parseAnimateOptions(options);
- element = angular.element(element);
-
- cancelChildAnimations(element);
- classBasedAnimationsBlocked(element, true);
- return runAnimationPostDigest(function(done) {
- return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() {
- $delegate.leave(element);
- }, options, done);
- });
- },
-
- /**
- * @ngdoc method
- * @name $animate#move
- * @kind function
- *
- * @description
- * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or
- * add the element directly after the afterElement element if present. Then the move animation will be run. Once
- * the animation is started, the following CSS classes will be added for the duration of the animation:
- *
- * Below is a breakdown of each step that occurs during move animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------|
- * | 1. `$animate.move(...)` is called | `class="my-animation"` |
- * | 2. element is moved into the parentElement element or beside the afterElement element | `class="my-animation"` |
- * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` |
- * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
- * | 5. the `.ng-move` class is added to the element | `class="my-animation ng-animate ng-move"` |
- * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-move"` |
- * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-move` class styling is applied right away | `class="my-animation ng-animate ng-move"` |
- * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-move"` |
- * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-move"` |
- * | 10. the `.ng-move-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-move ng-move-active"` |
- * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-move ng-move-active"` |
- * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
- * | 13. The returned promise is resolved. | `class="my-animation"` |
- *
- * @param {DOMElement} element the element that will be the focus of the move animation
- * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation
- * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation
- * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- move: function(element, parentElement, afterElement, options) {
- options = parseAnimateOptions(options);
- element = angular.element(element);
- parentElement = prepareElement(parentElement);
- afterElement = prepareElement(afterElement);
-
- cancelChildAnimations(element);
- classBasedAnimationsBlocked(element, true);
- $delegate.move(element, parentElement, afterElement);
- return runAnimationPostDigest(function(done) {
- return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done);
- });
- },
-
- /**
- * @ngdoc method
- * @name $animate#addClass
- *
- * @description
- * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class.
- * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide
- * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions
- * or keyframes are defined on the -add-active or base CSS class).
- *
- * Below is a breakdown of each step that occurs during addClass animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
- * | 1. `$animate.addClass(element, 'super')` is called | `class="my-animation"` |
- * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` |
- * | 3. the `.super-add` class is added to the element | `class="my-animation ng-animate super-add"` |
- * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate super-add"` |
- * | 5. the `.super` and `.super-add-active` classes are added (this triggers the CSS transition/animation) | `class="my-animation ng-animate super super-add super-add-active"` |
- * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super super-add super-add-active"` |
- * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super super-add super-add-active"` |
- * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation super"` |
- * | 9. The super class is kept on the element | `class="my-animation super"` |
- * | 10. The returned promise is resolved. | `class="my-animation super"` |
- *
- * @param {DOMElement} element the element that will be animated
- * @param {string} className the CSS class that will be added to the element and then animated
- * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- addClass: function(element, className, options) {
- return this.setClass(element, className, [], options);
- },
-
- /**
- * @ngdoc method
- * @name $animate#removeClass
- *
- * @description
- * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value
- * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in
- * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if
- * no CSS transitions or keyframes are defined on the -remove or base CSS classes).
- *
- * Below is a breakdown of each step that occurs during removeClass animation:
- *
- * | Animation Step | What the element class attribute looks like |
- * |----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
- * | 1. `$animate.removeClass(element, 'super')` is called | `class="my-animation super"` |
- * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation super ng-animate"` |
- * | 3. the `.super-remove` class is added to the element | `class="my-animation super ng-animate super-remove"` |
- * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation super ng-animate super-remove"` |
- * | 5. the `.super-remove-active` classes are added and `.super` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate super-remove super-remove-active"` |
- * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super-remove super-remove-active"` |
- * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super-remove super-remove-active"` |
- * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` |
- * | 9. The returned promise is resolved. | `class="my-animation"` |
- *
- *
- * @param {DOMElement} element the element that will be animated
- * @param {string} className the CSS class that will be animated and then removed from the element
- * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- removeClass: function(element, className, options) {
- return this.setClass(element, [], className, options);
- },
-
- /**
- *
- * @ngdoc method
- * @name $animate#setClass
- *
- * @description Adds and/or removes the given CSS classes to and from the element.
- * Once complete, the `done()` callback will be fired (if provided).
- *
- * | Animation Step | What the element class attribute looks like |
- * |----------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|
- * | 1. `$animate.setClass(element, 'on', 'off')` is called | `class="my-animation off"` |
- * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate off"` |
- * | 3. the `.on-add` and `.off-remove` classes are added to the element | `class="my-animation ng-animate on-add off-remove off"` |
- * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate on-add off-remove off"` |
- * | 5. the `.on`, `.on-add-active` and `.off-remove-active` classes are added and `.off` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
- * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
- * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` |
- * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation on"` |
- * | 9. The returned promise is resolved. | `class="my-animation on"` |
- *
- * @param {DOMElement} element the element which will have its CSS classes changed
- * removed from it
- * @param {string} add the CSS classes which will be added to the element
- * @param {string} remove the CSS class which will be removed from the element
- * CSS classes have been set on the element
- * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation
- * @return {Promise} the animation callback promise
- */
- setClass: function(element, add, remove, options) {
- options = parseAnimateOptions(options);
-
- var STORAGE_KEY = '$$animateClasses';
- element = angular.element(element);
- element = stripCommentsFromElement(element);
-
- if (classBasedAnimationsBlocked(element)) {
- return $delegate.$$setClassImmediately(element, add, remove, options);
- }
-
- // we're using a combined array for both the add and remove
- // operations since the ORDER OF addClass and removeClass matters
- var classes, cache = element.data(STORAGE_KEY);
- var hasCache = !!cache;
- if (!cache) {
- cache = {};
- cache.classes = {};
- }
- classes = cache.classes;
-
- add = isArray(add) ? add : add.split(' ');
- forEach(add, function(c) {
- if (c && c.length) {
- classes[c] = true;
- }
- });
-
- remove = isArray(remove) ? remove : remove.split(' ');
- forEach(remove, function(c) {
- if (c && c.length) {
- classes[c] = false;
- }
- });
-
- if (hasCache) {
- if (options && cache.options) {
- cache.options = angular.extend(cache.options || {}, options);
- }
-
- //the digest cycle will combine all the animations into one function
- return cache.promise;
- } else {
- element.data(STORAGE_KEY, cache = {
- classes: classes,
- options: options
- });
- }
-
- return cache.promise = runAnimationPostDigest(function(done) {
- var parentElement = element.parent();
- var elementNode = extractElementNode(element);
- var parentNode = elementNode.parentNode;
- // TODO(matsko): move this code into the animationsDisabled() function once #8092 is fixed
- if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) {
- done();
- return;
- }
-
- var cache = element.data(STORAGE_KEY);
- element.removeData(STORAGE_KEY);
-
- var state = element.data(NG_ANIMATE_STATE) || {};
- var classes = resolveElementClasses(element, cache, state.active);
- return !classes
- ? done()
- : performAnimation('setClass', classes, element, parentElement, null, function() {
- if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
- if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
- }, cache.options, done);
- });
- },
-
- /**
- * @ngdoc method
- * @name $animate#cancel
- * @kind function
- *
- * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
- *
- * @description
- * Cancels the provided animation.
- */
- cancel: function(promise) {
- promise.$$cancelFn();
- },
-
- /**
- * @ngdoc method
- * @name $animate#enabled
- * @kind function
- *
- * @param {boolean=} value If provided then set the animation on or off.
- * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation
- * @return {boolean} Current animation state.
- *
- * @description
- * Globally enables/disables animations.
- *
- */
- enabled: function(value, element) {
- switch (arguments.length) {
- case 2:
- if (value) {
- cleanup(element);
- } else {
- var data = element.data(NG_ANIMATE_STATE) || {};
- data.disabled = true;
- element.data(NG_ANIMATE_STATE, data);
- }
- break;
-
- case 1:
- rootAnimateState.disabled = !value;
- break;
-
- default:
- value = !rootAnimateState.disabled;
- break;
- }
- return !!value;
- }
- };
-
- /*
- all animations call this shared animation triggering function internally.
- The animationEvent variable refers to the JavaScript animation event that will be triggered
- and the className value is the name of the animation that will be applied within the
- CSS code. Element, `parentElement` and `afterElement` are provided DOM elements for the animation
- and the onComplete callback will be fired once the animation is fully complete.
- */
- function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) {
- var noopCancel = noop;
- var runner = animationRunner(element, animationEvent, className, options);
- if (!runner) {
- fireDOMOperation();
- fireBeforeCallbackAsync();
- fireAfterCallbackAsync();
- closeAnimation();
- return noopCancel;
- }
-
- animationEvent = runner.event;
- className = runner.className;
- var elementEvents = angular.element._data(runner.node);
- elementEvents = elementEvents && elementEvents.events;
-
- if (!parentElement) {
- parentElement = afterElement ? afterElement.parent() : element.parent();
- }
-
- //skip the animation if animations are disabled, a parent is already being animated,
- //the element is not currently attached to the document body or then completely close
- //the animation if any matching animations are not found at all.
- //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found.
- if (animationsDisabled(element, parentElement)) {
- fireDOMOperation();
- fireBeforeCallbackAsync();
- fireAfterCallbackAsync();
- closeAnimation();
- return noopCancel;
- }
-
- var ngAnimateState = element.data(NG_ANIMATE_STATE) || {};
- var runningAnimations = ngAnimateState.active || {};
- var totalActiveAnimations = ngAnimateState.totalActive || 0;
- var lastAnimation = ngAnimateState.last;
- var skipAnimation = false;
-
- if (totalActiveAnimations > 0) {
- var animationsToCancel = [];
- if (!runner.isClassBased) {
- if (animationEvent == 'leave' && runningAnimations['ng-leave']) {
- skipAnimation = true;
- } else {
- //cancel all animations when a structural animation takes place
- for (var klass in runningAnimations) {
- animationsToCancel.push(runningAnimations[klass]);
- }
- ngAnimateState = {};
- cleanup(element, true);
- }
- } else if (lastAnimation.event == 'setClass') {
- animationsToCancel.push(lastAnimation);
- cleanup(element, className);
- } else if (runningAnimations[className]) {
- var current = runningAnimations[className];
- if (current.event == animationEvent) {
- skipAnimation = true;
- } else {
- animationsToCancel.push(current);
- cleanup(element, className);
- }
- }
-
- if (animationsToCancel.length > 0) {
- forEach(animationsToCancel, function(operation) {
- operation.cancel();
- });
- }
- }
-
- if (runner.isClassBased
- && !runner.isSetClassOperation
- && animationEvent != 'animate'
- && !skipAnimation) {
- skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR
- }
-
- if (skipAnimation) {
- fireDOMOperation();
- fireBeforeCallbackAsync();
- fireAfterCallbackAsync();
- fireDoneCallbackAsync();
- return noopCancel;
- }
-
- runningAnimations = ngAnimateState.active || {};
- totalActiveAnimations = ngAnimateState.totalActive || 0;
-
- if (animationEvent == 'leave') {
- //there's no need to ever remove the listener since the element
- //will be removed (destroyed) after the leave animation ends or
- //is cancelled midway
- element.one('$destroy', function(e) {
- var element = angular.element(this);
- var state = element.data(NG_ANIMATE_STATE);
- if (state) {
- var activeLeaveAnimation = state.active['ng-leave'];
- if (activeLeaveAnimation) {
- activeLeaveAnimation.cancel();
- cleanup(element, 'ng-leave');
- }
- }
- });
- }
-
- //the ng-animate class does nothing, but it's here to allow for
- //parent animations to find and cancel child animations when needed
- $$jqLite.addClass(element, NG_ANIMATE_CLASS_NAME);
- if (options && options.tempClasses) {
- forEach(options.tempClasses, function(className) {
- $$jqLite.addClass(element, className);
- });
- }
-
- var localAnimationCount = globalAnimationCounter++;
- totalActiveAnimations++;
- runningAnimations[className] = runner;
-
- element.data(NG_ANIMATE_STATE, {
- last: runner,
- active: runningAnimations,
- index: localAnimationCount,
- totalActive: totalActiveAnimations
- });
-
- //first we run the before animations and when all of those are complete
- //then we perform the DOM operation and run the next set of animations
- fireBeforeCallbackAsync();
- runner.before(function(cancelled) {
- var data = element.data(NG_ANIMATE_STATE);
- cancelled = cancelled ||
- !data || !data.active[className] ||
- (runner.isClassBased && data.active[className].event != animationEvent);
-
- fireDOMOperation();
- if (cancelled === true) {
- closeAnimation();
- } else {
- fireAfterCallbackAsync();
- runner.after(closeAnimation);
- }
- });
-
- return runner.cancel;
-
- function fireDOMCallback(animationPhase) {
- var eventName = '$animate:' + animationPhase;
- if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) {
- $$asyncCallback(function() {
- element.triggerHandler(eventName, {
- event: animationEvent,
- className: className
- });
- });
- }
- }
-
- function fireBeforeCallbackAsync() {
- fireDOMCallback('before');
- }
-
- function fireAfterCallbackAsync() {
- fireDOMCallback('after');
- }
-
- function fireDoneCallbackAsync() {
- fireDOMCallback('close');
- doneCallback();
- }
-
- //it is less complicated to use a flag than managing and canceling
- //timeouts containing multiple callbacks.
- function fireDOMOperation() {
- if (!fireDOMOperation.hasBeenRun) {
- fireDOMOperation.hasBeenRun = true;
- domOperation();
- }
- }
-
- function closeAnimation() {
- if (!closeAnimation.hasBeenRun) {
- if (runner) { //the runner doesn't exist if it fails to instantiate
- runner.applyStyles();
- }
-
- closeAnimation.hasBeenRun = true;
- if (options && options.tempClasses) {
- forEach(options.tempClasses, function(className) {
- $$jqLite.removeClass(element, className);
- });
- }
-
- var data = element.data(NG_ANIMATE_STATE);
- if (data) {
-
- /* only structural animations wait for reflow before removing an
- animation, but class-based animations don't. An example of this
- failing would be when a parent HTML tag has a ng-class attribute
- causing ALL directives below to skip animations during the digest */
- if (runner && runner.isClassBased) {
- cleanup(element, className);
- } else {
- $$asyncCallback(function() {
- var data = element.data(NG_ANIMATE_STATE) || {};
- if (localAnimationCount == data.index) {
- cleanup(element, className, animationEvent);
- }
- });
- element.data(NG_ANIMATE_STATE, data);
- }
- }
- fireDoneCallbackAsync();
- }
- }
- }
-
- function cancelChildAnimations(element) {
- var node = extractElementNode(element);
- if (node) {
- var nodes = angular.isFunction(node.getElementsByClassName) ?
- node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) :
- node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME);
- forEach(nodes, function(element) {
- element = angular.element(element);
- var data = element.data(NG_ANIMATE_STATE);
- if (data && data.active) {
- forEach(data.active, function(runner) {
- runner.cancel();
- });
- }
- });
- }
- }
-
- function cleanup(element, className) {
- if (isMatchingElement(element, $rootElement)) {
- if (!rootAnimateState.disabled) {
- rootAnimateState.running = false;
- rootAnimateState.structural = false;
- }
- } else if (className) {
- var data = element.data(NG_ANIMATE_STATE) || {};
-
- var removeAnimations = className === true;
- if (!removeAnimations && data.active && data.active[className]) {
- data.totalActive--;
- delete data.active[className];
- }
-
- if (removeAnimations || !data.totalActive) {
- $$jqLite.removeClass(element, NG_ANIMATE_CLASS_NAME);
- element.removeData(NG_ANIMATE_STATE);
- }
- }
- }
-
- function animationsDisabled(element, parentElement) {
- if (rootAnimateState.disabled) {
- return true;
- }
-
- if (isMatchingElement(element, $rootElement)) {
- return rootAnimateState.running;
- }
-
- var allowChildAnimations, parentRunningAnimation, hasParent;
- do {
- //the element did not reach the root element which means that it
- //is not apart of the DOM. Therefore there is no reason to do
- //any animations on it
- if (parentElement.length === 0) break;
-
- var isRoot = isMatchingElement(parentElement, $rootElement);
- var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {});
- if (state.disabled) {
- return true;
- }
-
- //no matter what, for an animation to work it must reach the root element
- //this implies that the element is attached to the DOM when the animation is run
- if (isRoot) {
- hasParent = true;
- }
-
- //once a flag is found that is strictly false then everything before
- //it will be discarded and all child animations will be restricted
- if (allowChildAnimations !== false) {
- var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN);
- if (angular.isDefined(animateChildrenFlag)) {
- allowChildAnimations = animateChildrenFlag;
- }
- }
-
- parentRunningAnimation = parentRunningAnimation ||
- state.running ||
- (state.last && !state.last.isClassBased);
- }
- while (parentElement = parentElement.parent());
-
- return !hasParent || (!allowChildAnimations && parentRunningAnimation);
- }
- }]);
-
- $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow',
- function($window, $sniffer, $timeout, $$animateReflow) {
- // 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 ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
- var ANIMATION_PLAYSTATE_KEY = 'PlayState';
- var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey';
- var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data';
- var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
- var CLOSING_TIME_BUFFER = 1.5;
- var ONE_SECOND = 1000;
-
- var lookupCache = {};
- var parentCounter = 0;
- var animationReflowQueue = [];
- var cancelAnimationReflow;
- function clearCacheAfterReflow() {
- if (!cancelAnimationReflow) {
- cancelAnimationReflow = $$animateReflow(function() {
- animationReflowQueue = [];
- cancelAnimationReflow = null;
- lookupCache = {};
- });
- }
- }
-
- function afterReflow(element, callback) {
- if (cancelAnimationReflow) {
- cancelAnimationReflow();
- }
- animationReflowQueue.push(callback);
- cancelAnimationReflow = $$animateReflow(function() {
- forEach(animationReflowQueue, function(fn) {
- fn();
- });
-
- animationReflowQueue = [];
- cancelAnimationReflow = null;
- lookupCache = {};
- });
- }
-
- var closingTimer = null;
- var closingTimestamp = 0;
- var animationElementQueue = [];
- function animationCloseHandler(element, totalTime) {
- var node = extractElementNode(element);
- element = angular.element(node);
-
- //this item will be garbage collected by the closing
- //animation timeout
- animationElementQueue.push(element);
-
- //but it may not need to cancel out the existing timeout
- //if the timestamp is less than the previous one
- var futureTimestamp = Date.now() + totalTime;
- if (futureTimestamp <= closingTimestamp) {
- return;
- }
-
- $timeout.cancel(closingTimer);
-
- closingTimestamp = futureTimestamp;
- closingTimer = $timeout(function() {
- closeAllAnimations(animationElementQueue);
- animationElementQueue = [];
- }, totalTime, false);
- }
-
- function closeAllAnimations(elements) {
- forEach(elements, function(element) {
- var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
- if (elementData) {
- forEach(elementData.closeAnimationFns, function(fn) {
- fn();
- });
- }
- });
- }
-
- function getElementAnimationDetails(element, cacheKey) {
- var data = cacheKey ? lookupCache[cacheKey] : null;
- if (!data) {
- var transitionDuration = 0;
- var transitionDelay = 0;
- var animationDuration = 0;
- var animationDelay = 0;
-
- //we want all the styles defined before and after
- forEach(element, function(element) {
- if (element.nodeType == ELEMENT_NODE) {
- var elementStyles = $window.getComputedStyle(element) || {};
-
- var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY];
- transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration);
-
- var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY];
- transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay);
-
- var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY];
- animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay);
-
- var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]);
-
- if (aDuration > 0) {
- aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1;
- }
- animationDuration = Math.max(aDuration, animationDuration);
- }
- });
- data = {
- total: 0,
- transitionDelay: transitionDelay,
- transitionDuration: transitionDuration,
- animationDelay: animationDelay,
- animationDuration: animationDuration
- };
- if (cacheKey) {
- lookupCache[cacheKey] = data;
- }
- }
- return data;
- }
-
- function parseMaxTime(str) {
- var maxValue = 0;
- var values = isString(str) ?
- str.split(/\s*,\s*/) :
- [];
- forEach(values, function(value) {
- maxValue = Math.max(parseFloat(value) || 0, maxValue);
- });
- return maxValue;
- }
-
- function getCacheKey(element) {
- var parentElement = element.parent();
- var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY);
- if (!parentID) {
- parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter);
- parentID = parentCounter;
- }
- return parentID + '-' + extractElementNode(element).getAttribute('class');
- }
-
- function animateSetup(animationEvent, element, className, styles) {
- var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0;
-
- var cacheKey = getCacheKey(element);
- var eventCacheKey = cacheKey + ' ' + className;
- var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0;
-
- var stagger = {};
- if (itemIndex > 0) {
- var staggerClassName = className + '-stagger';
- var staggerCacheKey = cacheKey + ' ' + staggerClassName;
- var applyClasses = !lookupCache[staggerCacheKey];
-
- applyClasses && $$jqLite.addClass(element, staggerClassName);
-
- stagger = getElementAnimationDetails(element, staggerCacheKey);
-
- applyClasses && $$jqLite.removeClass(element, staggerClassName);
- }
-
- $$jqLite.addClass(element, className);
-
- var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {};
- var timings = getElementAnimationDetails(element, eventCacheKey);
- var transitionDuration = timings.transitionDuration;
- var animationDuration = timings.animationDuration;
-
- if (structural && transitionDuration === 0 && animationDuration === 0) {
- $$jqLite.removeClass(element, className);
- return false;
- }
-
- var blockTransition = styles || (structural && transitionDuration > 0);
- var blockAnimation = animationDuration > 0 &&
- stagger.animationDelay > 0 &&
- stagger.animationDuration === 0;
-
- var closeAnimationFns = formerData.closeAnimationFns || [];
- element.data(NG_ANIMATE_CSS_DATA_KEY, {
- stagger: stagger,
- cacheKey: eventCacheKey,
- running: formerData.running || 0,
- itemIndex: itemIndex,
- blockTransition: blockTransition,
- closeAnimationFns: closeAnimationFns
- });
-
- var node = extractElementNode(element);
-
- if (blockTransition) {
- blockTransitions(node, true);
- if (styles) {
- element.css(styles);
- }
- }
-
- if (blockAnimation) {
- blockAnimations(node, true);
- }
-
- return true;
- }
-
- function animateRun(animationEvent, element, className, activeAnimationComplete, styles) {
- var node = extractElementNode(element);
- var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY);
- if (node.getAttribute('class').indexOf(className) == -1 || !elementData) {
- activeAnimationComplete();
- return;
- }
-
- var activeClassName = '';
- var pendingClassName = '';
- forEach(className.split(' '), function(klass, i) {
- var prefix = (i > 0 ? ' ' : '') + klass;
- activeClassName += prefix + '-active';
- pendingClassName += prefix + '-pending';
- });
-
- var style = '';
- var appliedStyles = [];
- var itemIndex = elementData.itemIndex;
- var stagger = elementData.stagger;
- var staggerTime = 0;
- if (itemIndex > 0) {
- var transitionStaggerDelay = 0;
- if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) {
- transitionStaggerDelay = stagger.transitionDelay * itemIndex;
- }
-
- var animationStaggerDelay = 0;
- if (stagger.animationDelay > 0 && stagger.animationDuration === 0) {
- animationStaggerDelay = stagger.animationDelay * itemIndex;
- appliedStyles.push(CSS_PREFIX + 'animation-play-state');
- }
-
- staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100;
- }
-
- if (!staggerTime) {
- $$jqLite.addClass(element, activeClassName);
- if (elementData.blockTransition) {
- blockTransitions(node, false);
- }
- }
-
- var eventCacheKey = elementData.cacheKey + ' ' + activeClassName;
- var timings = getElementAnimationDetails(element, eventCacheKey);
- var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);
- if (maxDuration === 0) {
- $$jqLite.removeClass(element, activeClassName);
- animateClose(element, className);
- activeAnimationComplete();
- return;
- }
-
- if (!staggerTime && styles && Object.keys(styles).length > 0) {
- if (!timings.transitionDuration) {
- element.css('transition', timings.animationDuration + 's linear all');
- appliedStyles.push('transition');
- }
- element.css(styles);
- }
-
- var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay);
- var maxDelayTime = maxDelay * ONE_SECOND;
-
- if (appliedStyles.length > 0) {
- //the element being animated may sometimes contain comment nodes in
- //the jqLite object, so we're safe to use a single variable to house
- //the styles since there is always only one element being animated
- var oldStyle = node.getAttribute('style') || '';
- if (oldStyle.charAt(oldStyle.length - 1) !== ';') {
- oldStyle += ';';
- }
- node.setAttribute('style', oldStyle + ' ' + style);
- }
-
- var startTime = Date.now();
- var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT;
- var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER;
- var totalTime = (staggerTime + animationTime) * ONE_SECOND;
-
- var staggerTimeout;
- if (staggerTime > 0) {
- $$jqLite.addClass(element, pendingClassName);
- staggerTimeout = $timeout(function() {
- staggerTimeout = null;
-
- if (timings.transitionDuration > 0) {
- blockTransitions(node, false);
- }
- if (timings.animationDuration > 0) {
- blockAnimations(node, false);
- }
-
- $$jqLite.addClass(element, activeClassName);
- $$jqLite.removeClass(element, pendingClassName);
-
- if (styles) {
- if (timings.transitionDuration === 0) {
- element.css('transition', timings.animationDuration + 's linear all');
- }
- element.css(styles);
- appliedStyles.push('transition');
- }
- }, staggerTime * ONE_SECOND, false);
- }
-
- element.on(css3AnimationEvents, onAnimationProgress);
- elementData.closeAnimationFns.push(function() {
- onEnd();
- activeAnimationComplete();
- });
-
- elementData.running++;
- animationCloseHandler(element, totalTime);
- return onEnd;
-
- // This will automatically be called by $animate so
- // there is no need to attach this internally to the
- // timeout done method.
- function onEnd() {
- element.off(css3AnimationEvents, onAnimationProgress);
- $$jqLite.removeClass(element, activeClassName);
- $$jqLite.removeClass(element, pendingClassName);
- if (staggerTimeout) {
- $timeout.cancel(staggerTimeout);
- }
- animateClose(element, className);
- var node = extractElementNode(element);
- for (var i in appliedStyles) {
- node.style.removeProperty(appliedStyles[i]);
- }
- }
-
- function onAnimationProgress(event) {
- event.stopPropagation();
- var ev = event.originalEvent || event;
- var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
-
- /* Firefox (or possibly just Gecko) likes to not round values up
- * when a ms measurement is used for the animation */
- var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
-
- /* $manualTimeStamp is a mocked timeStamp value which is set
- * within browserTrigger(). This is only here so that tests can
- * mock animations properly. Real events fallback to event.timeStamp,
- * or, if they don't, then a timeStamp is automatically created for them.
- * We're checking to see if the timeStamp surpasses the expected delay,
- * but we're using elapsedTime instead of the timeStamp on the 2nd
- * pre-condition since animations sometimes close off early */
- if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
- activeAnimationComplete();
- }
- }
- }
-
- function blockTransitions(node, bool) {
- node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : '';
- }
-
- function blockAnimations(node, bool) {
- node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : '';
- }
-
- function animateBefore(animationEvent, element, className, styles) {
- if (animateSetup(animationEvent, element, className, styles)) {
- return function(cancelled) {
- cancelled && animateClose(element, className);
- };
- }
- }
-
- function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) {
- if (element.data(NG_ANIMATE_CSS_DATA_KEY)) {
- return animateRun(animationEvent, element, className, afterAnimationComplete, styles);
- } else {
- animateClose(element, className);
- afterAnimationComplete();
- }
- }
-
- function animate(animationEvent, element, className, animationComplete, options) {
- //If the animateSetup function doesn't bother returning a
- //cancellation function then it means that there is no animation
- //to perform at all
- var preReflowCancellation = animateBefore(animationEvent, element, className, options.from);
- if (!preReflowCancellation) {
- clearCacheAfterReflow();
- animationComplete();
- return;
- }
-
- //There are two cancellation functions: one is before the first
- //reflow animation and the second is during the active state
- //animation. The first function will take care of removing the
- //data from the element which will not make the 2nd animation
- //happen in the first place
- var cancel = preReflowCancellation;
- afterReflow(element, function() {
- //once the reflow is complete then we point cancel to
- //the new cancellation function which will remove all of the
- //animation properties from the active animation
- cancel = animateAfter(animationEvent, element, className, animationComplete, options.to);
- });
-
- return function(cancelled) {
- (cancel || noop)(cancelled);
- };
- }
-
- function animateClose(element, className) {
- $$jqLite.removeClass(element, className);
- var data = element.data(NG_ANIMATE_CSS_DATA_KEY);
- if (data) {
- if (data.running) {
- data.running--;
- }
- if (!data.running || data.running === 0) {
- element.removeData(NG_ANIMATE_CSS_DATA_KEY);
- }
- }
- }
-
- return {
- animate: function(element, className, from, to, animationCompleted, options) {
- options = options || {};
- options.from = from;
- options.to = to;
- return animate('animate', element, className, animationCompleted, options);
- },
-
- enter: function(element, animationCompleted, options) {
- options = options || {};
- return animate('enter', element, 'ng-enter', animationCompleted, options);
- },
-
- leave: function(element, animationCompleted, options) {
- options = options || {};
- return animate('leave', element, 'ng-leave', animationCompleted, options);
- },
-
- move: function(element, animationCompleted, options) {
- options = options || {};
- return animate('move', element, 'ng-move', animationCompleted, options);
- },
-
- beforeSetClass: function(element, add, remove, animationCompleted, options) {
- options = options || {};
- var className = suffixClasses(remove, '-remove') + ' ' +
- suffixClasses(add, '-add');
- var cancellationMethod = animateBefore('setClass', element, className, options.from);
- if (cancellationMethod) {
- afterReflow(element, animationCompleted);
- return cancellationMethod;
- }
- clearCacheAfterReflow();
- animationCompleted();
- },
-
- beforeAddClass: function(element, className, animationCompleted, options) {
- options = options || {};
- var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from);
- if (cancellationMethod) {
- afterReflow(element, animationCompleted);
- return cancellationMethod;
- }
- clearCacheAfterReflow();
- animationCompleted();
- },
-
- beforeRemoveClass: function(element, className, animationCompleted, options) {
- options = options || {};
- var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from);
- if (cancellationMethod) {
- afterReflow(element, animationCompleted);
- return cancellationMethod;
- }
- clearCacheAfterReflow();
- animationCompleted();
- },
-
- setClass: function(element, add, remove, animationCompleted, options) {
- options = options || {};
- remove = suffixClasses(remove, '-remove');
- add = suffixClasses(add, '-add');
- var className = remove + ' ' + add;
- return animateAfter('setClass', element, className, animationCompleted, options.to);
- },
-
- addClass: function(element, className, animationCompleted, options) {
- options = options || {};
- return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to);
- },
-
- removeClass: function(element, className, animationCompleted, options) {
- options = options || {};
- return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to);
- }
- };
-
- function suffixClasses(classes, suffix) {
- var className = '';
- classes = isArray(classes) ? classes : classes.split(/\s+/);
- forEach(classes, function(klass, i) {
- if (klass && klass.length > 0) {
- className += (i > 0 ? ' ' : '') + klass + suffix;
- }
- });
- return className;
- }
- }]);
- }]);
diff --git a/src/ngAnimate/animateChildrenDirective.js b/src/ngAnimate/animateChildrenDirective.js
new file mode 100644
index 000000000000..3962a632d890
--- /dev/null
+++ b/src/ngAnimate/animateChildrenDirective.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var $$AnimateChildrenDirective = [function() {
+ return function(scope, element, attrs) {
+ var val = attrs.ngAnimateChildren;
+ if (angular.isString(val) && val.length === 0) { //empty attribute
+ element.data(NG_ANIMATE_CHILDREN_DATA, true);
+ } else {
+ attrs.$observe('ngAnimateChildren', function(value) {
+ value = value === 'on' || value === 'true';
+ element.data(NG_ANIMATE_CHILDREN_DATA, value);
+ });
+ }
+ };
+}];
diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js
new file mode 100644
index 000000000000..1280da9b714b
--- /dev/null
+++ b/src/ngAnimate/animateCss.js
@@ -0,0 +1,997 @@
+'use strict';
+
+/**
+ * @ngdoc service
+ * @name $animateCss
+ * @kind object
+ *
+ * @description
+ * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes
+ * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT
+ * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or
+ * 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
+ * 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
+ * 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`:
+ *
+ * ```html
+ *
+ *
+ * This element will go BOOM
+ *
+ *
+ * ```
+ *
+ * Now we create the **JavaScript animation** that will trigger the CSS transition:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * var animation = $animateCss(element, {
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ *
+ * // if no possible animation can be triggered due
+ * // to the combination of options then `animation`
+ * // will be returned as undefined
+ * animation.start().done(doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * ## More Advanced Uses
+ *
+ * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks
+ * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code.
+ *
+ * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation,
+ * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with
+ * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order
+ * to provide a working animation that will run in CSS.
+ *
+ * The example below showcases a more advanced version of the `.fold-animation` from the example above:
+ *
+ * ```js
+ * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var height = element[0].offsetHeight;
+ * var animation = $animateCss(element, {
+ * addClass: 'red large-text pulse-twice',
+ * easing: 'ease-out',
+ * from: { height:'0px' },
+ * to: { height:height + 'px' },
+ * duration: 1 // one second
+ * });
+ *
+ * // if no possible animation can be triggered due
+ * // to the combination of options then `animation`
+ * // will be returned as undefined
+ * animation.start().done(doneFn);
+ * }
+ * }
+ * }]);
+ * ```
+ *
+ * Since we're adding/removing CSS classes then the CSS transition will also pick those up:
+ *
+ * ```css
+ * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code,
+ * the CSS classes below will be transitioned despite them being defined as regular CSS classes */
+ * .red { background:red; }
+ * .large-text { font-size:20px; }
+ *
+ * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */
+ * .pulse-twice {
+ * animation: 0.5s pulse linear 2;
+ * -webkit-animation: 0.5s pulse linear 2;
+ * }
+ *
+ * @keyframes pulse {
+ * from { transform: scale(0.5); }
+ * to { transform: scale(1.5); }
+ * }
+ *
+ * @-webkit-keyframes pulse {
+ * from { -webkit-transform: scale(0.5); }
+ * to { -webkit-transform: scale(1.5); }
+ * }
+ * ```
+ *
+ * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen.
+ *
+ * ## How the Options are handled
+ *
+ * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation
+ * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline
+ * styles using the `from` and `to` properties.
+ *
+ * ```js
+ * var animation = $animateCss(element, {
+ * from: { background:'red' },
+ * to: { background:'blue' }
+ * });
+ * ```
+ *
+ * ```css
+ * .rotating-animation {
+ * animation:0.5s rotate linear;
+ * -webkit-animation:0.5s rotate linear;
+ * }
+ *
+ * @keyframes rotate {
+ * from { transform: rotate(0deg); }
+ * to { transform: rotate(360deg); }
+ * }
+ *
+ * @-webkit-keyframes rotate {
+ * from { -webkit-transform: rotate(0deg); }
+ * to { -webkit-transform: rotate(360deg); }
+ * }
+ * ```
+ *
+ * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is
+ * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition
+ * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition
+ * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied
+ * and spread across the transition and keyframe animation.
+ *
+ * ## What is returned
+ *
+ * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually
+ * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are
+ * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties:
+ *
+ * ```js
+ * var animation = $animateCss(element, { ... });
+ * ```
+ *
+ * Now what do the contents of our `animation` variable look like:
+ *
+ * ```js
+ * {
+ * // starts the animation
+ * start: Function,
+ *
+ * // ends (aborts) the animation
+ * end: Function,
+ *
+ * // the total number of seconds that the animation will run for
+ * duration: Number,
+ *
+ * // the total number of seconds that the animation will delay for before starting
+ * delay: Number,
+ *
+ * // whether or not transitions were detected and will therefore be used for the animation
+ * transitions: Boolean,
+ *
+ * // whether or not keyframe animations were detected and will therefore be used for the animation
+ * keyframes: Boolean
+ * }
+ * ```
+ *
+ * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends.
+ * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and stlyes may have been
+ * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties
+ * and that changing them will not reconfigure the parameters of the animation.
+ *
+ * By calling `animation.start()` we do get back a promise, however, due to the nature of animations we may not want to tap into the default behaviour of
+ * animations (since they cause a digest to occur which may slow down the animation performance-wise). Therefore instead of calling `then` to capture when
+ * the animation ends be sure to call `done(callback)` (this is the recommended way to use `$animateCss` within JavaScript-animations).
+ *
+ * The example below should put this into perspective:
+ *
+ * ```js
+ * var animation = $animateCss(element, { ... });
+ *
+ * // remember that if there is no CSS animation detected on the element
+ * // then the value returned from $animateCss will be null
+ * if (animation) {
+ * animation.start().done(function() {
+ * // yaay the animation is over
+ * doneCallback();
+ * });
+ * } else {
+ * doneCallback();
+ * }
+ * ```
+ *
+ * @param {DOMElement} element the element that will be animated
+ * @param {object} options the animation-related options that will be applied during the animation
+ *
+ * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied
+ * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.)
+ * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
+ * * `transition` - The raw CSS transition style that will be used (e.g. `1s linear all`).
+ * * `keyframe` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
+ * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation.
+ * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition.
+ * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation.
+ * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation.
+ * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0`
+ * is provided then the animation will be skipped entirely.
+ * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is
+ * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value
+ * 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`)
+ *
+ * @return {null|object} an object with a start method and details about the animation. If no animation is detected then a value of `null` will be returned.
+ *
+ * * `start` - The method to start the animation. This will return a `Promise` when called.
+ * * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
+ */
+
+// Detect proper transitionend/animationend event names.
+var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
+
+// If unprefixed events are not supported but webkit-prefixed are, use the latter.
+// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
+// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
+// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
+// Register both events in case `window.onanimationend` is not supported because of that,
+// do the same for `transitionend` as Safari is likely to exhibit similar behavior.
+// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
+// therefore there is no reason to test anymore for other vendor prefixes:
+// http://caniuse.com/#search=transition
+if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ TRANSITION_PROP = 'WebkitTransition';
+ TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
+} else {
+ TRANSITION_PROP = 'transition';
+ TRANSITIONEND_EVENT = 'transitionend';
+}
+
+if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) {
+ CSS_PREFIX = '-webkit-';
+ ANIMATION_PROP = 'WebkitAnimation';
+ ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
+} else {
+ ANIMATION_PROP = 'animation';
+ ANIMATIONEND_EVENT = 'animationend';
+}
+
+var DURATION_KEY = 'Duration';
+var PROPERTY_KEY = 'Property';
+var DELAY_KEY = 'Delay';
+var TIMING_KEY = 'TimingFunction';
+var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
+var ANIMATION_PLAYSTATE_KEY = 'PlayState';
+var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
+var CLOSING_TIME_BUFFER = 1.5;
+var ONE_SECOND = 1000;
+var BASE_TEN = 10;
+
+var SAFE_FAST_FORWARD_DURATION_VALUE = 9999;
+
+var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY;
+var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY;
+
+var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY;
+var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
+
+var DETECT_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ transitionProperty: TRANSITION_PROP + PROPERTY_KEY,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP,
+ animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY
+};
+
+var DETECT_STAGGER_CSS_PROPERTIES = {
+ transitionDuration: TRANSITION_DURATION_PROP,
+ transitionDelay: TRANSITION_DELAY_PROP,
+ animationDuration: ANIMATION_DURATION_PROP,
+ animationDelay: ANIMATION_DELAY_PROP
+};
+
+function computeCssStyles($window, element, properties) {
+ var styles = Object.create(null);
+ var detectedStyles = $window.getComputedStyle(element) || {};
+ forEach(properties, function(formalStyleName, actualStyleName) {
+ var val = detectedStyles[formalStyleName];
+ if (val) {
+ var c = val.charAt(0);
+
+ // only numerical-based values have a negative sign or digit as the first value
+ if (c === '-' || c === '+' || c >= 0) {
+ val = parseMaxTime(val);
+ }
+
+ // by setting this to null in the event that the delay is not set or is set directly as 0
+ // then we can still allow for zegative values to be used later on and not mistake this
+ // value for being greater than any other negative value.
+ if (val === 0) {
+ val = null;
+ }
+ styles[actualStyleName] = val;
+ }
+ });
+
+ return styles;
+}
+
+function parseMaxTime(str) {
+ 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') {
+ value = value.substring(0, value.length - 1);
+ }
+ value = parseFloat(value) || 0;
+ maxValue = maxValue ? Math.max(value, maxValue) : value;
+ });
+ return maxValue;
+}
+
+function truthyTimingValue(val) {
+ return val === 0 || val != null;
+}
+
+function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
+ var style = TRANSITION_PROP;
+ var value = duration + 's';
+ if (applyOnlyDuration) {
+ style += DURATION_KEY;
+ } else {
+ 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);
+ },
+
+ 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++;
+ }
+ }
+ };
+}
+
+var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
+ var gcsLookup = createLocalCacheLookup();
+ var gcsStaggerLookup = createLocalCacheLookup();
+
+ this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
+ '$document', '$sniffer', '$$rAF',
+ function($window, $$jqLite, $$AnimateRunner, $timeout,
+ $document, $sniffer, $$rAF) {
+
+ 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, properties) {
+ var timings = gcsLookup.get(cacheKey);
+
+ if (!timings) {
+ timings = computeCssStyles($window, node, properties);
+ if (timings.animationIterationCount === 'infinite') {
+ timings.animationIterationCount = 1;
+ }
+ }
+
+ // we keep putting this in multiple times even though the value and the cacheKey are the same
+ // because we're keeping an interal tally of how many duplicate animations are detected.
+ gcsLookup.put(cacheKey, timings);
+ return timings;
+ }
+
+ function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
+ var stagger;
+
+ // 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 (!stagger) {
+ var staggerClassName = pendClasses(className, '-stagger');
+
+ $$jqLite.addClass(node, staggerClassName);
+
+ stagger = computeCssStyles($window, node, properties);
+
+ // force the conversion of a null value to zero incase not set
+ stagger.animationDuration = Math.max(stagger.animationDuration, 0);
+ stagger.transitionDuration = Math.max(stagger.transitionDuration, 0);
+
+ $$jqLite.removeClass(node, staggerClassName);
+
+ gcsStaggerLookup.put(cacheKey, stagger);
+ }
+ }
+
+ return stagger || {};
+ }
+
+ var bod = $document[0].body;
+ var cancelLastRAFRequest;
+ var rafWaitQueue = [];
+ function waitUntilQuiet(callback) {
+ if (cancelLastRAFRequest) {
+ cancelLastRAFRequest(); //cancels the request
+ }
+ rafWaitQueue.push(callback);
+ 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;
+ forEach(rafWaitQueue, function(cb) {
+ cb(width);
+ });
+ rafWaitQueue.length = 0;
+ });
+ }
+
+ return init;
+
+ function computeTimings(node, className, cacheKey) {
+ var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
+ var aD = timings.animationDelay;
+ var tD = timings.transitionDelay;
+ timings.maxDelay = aD && tD
+ ? Math.max(aD, tD)
+ : (aD || tD);
+ timings.maxDuration = Math.max(
+ timings.animationDuration * timings.animationIterationCount,
+ timings.transitionDuration);
+
+ return timings;
+ }
+
+ function init(element, options) {
+ var node = element[0];
+ options = prepareAnimationOptions(options);
+
+ var temporaryStyles = [];
+ var classes = element.attr('class');
+ var styles = packageStyles(options);
+ var animationClosed;
+ var animationPaused;
+ var animationCompleted;
+ var runner;
+ var runnerHost;
+ var maxDelay;
+ var maxDelayTime;
+ var maxDuration;
+ var maxDurationTime;
+
+ if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) {
+ close();
+ return;
+ }
+
+ var method = options.event && isArray(options.event)
+ ? options.event.join(' ')
+ : options.event;
+
+ var isStructural = method && options.structural;
+ var structuralClassName = '';
+ var addRemoveClassName = '';
+
+ if (isStructural) {
+ structuralClassName = pendClasses(method, 'ng-', true);
+ } else if (method) {
+ structuralClassName = method;
+ }
+
+ if (options.addClass) {
+ addRemoveClassName += pendClasses(options.addClass, '-add');
+ }
+
+ if (options.removeClass) {
+ if (addRemoveClassName.length) {
+ addRemoveClassName += ' ';
+ }
+ addRemoveClassName += pendClasses(options.removeClass, '-remove');
+ }
+
+ var setupClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
+ var fullClassName = classes + ' ' + setupClasses;
+ var activeClasses = pendClasses(setupClasses, '-active');
+ var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
+
+ // there is no way we can trigger an animation since no styles and
+ // no classes are being applied which would then trigger a transition
+ if (!hasToStyles && !setupClasses) {
+ close();
+ return false;
+ }
+
+ var cacheKey, stagger;
+ 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, setupClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
+ }
+
+ $$jqLite.addClass(element, setupClasses);
+
+ var applyOnlyDuration;
+
+ if (options.transitionStyle) {
+ var transitionStyle = [TRANSITION_PROP, options.transitionStyle];
+ applyInlineStyle(node, transitionStyle);
+ temporaryStyles.push(transitionStyle);
+ }
+
+ if (options.duration >= 0) {
+ applyOnlyDuration = node.style[TRANSITION_PROP].length > 0;
+ var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration);
+
+ // we set the duration so that it will be picked up by getComputedStyle later
+ applyInlineStyle(node, durationStyle);
+ temporaryStyles.push(durationStyle);
+ }
+
+ if (options.keyframeStyle) {
+ var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle];
+ applyInlineStyle(node, keyframeStyle);
+ temporaryStyles.push(keyframeStyle);
+ }
+
+ var itemIndex = stagger
+ ? options.staggerIndex >= 0
+ ? options.staggerIndex
+ : gcsLookup.count(cacheKey)
+ : 0;
+
+ var isFirst = itemIndex === 0;
+
+ // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY
+ // 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) {
+ blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
+ }
+
+ var timings = computeTimings(node, fullClassName, cacheKey);
+ 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.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);
+ flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations;
+ flags.recalculateTimingStyles = addRemoveClassName.length > 0;
+
+ if (flags.applyTransitionDuration || flags.applyAnimationDuration) {
+ maxDuration = options.duration ? parseFloat(options.duration) : maxDuration;
+
+ if (flags.applyTransitionDuration) {
+ flags.hasTransitions = true;
+ timings.transitionDuration = maxDuration;
+ applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0;
+ temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration));
+ }
+
+ if (flags.applyAnimationDuration) {
+ flags.hasAnimations = true;
+ timings.animationDuration = maxDuration;
+ temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration));
+ }
+ }
+
+ flags.transitionClassBlock = timings.transitionProperty === 'none' &&
+ timings.transitionDuration === 0;
+
+ // 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
+ // from happening (by setting 0s none in the class name). If this is the case
+ // we need to apply the classes before the first rAF so we know to continue if
+ // there actually is a detected transition or keyframe animation
+ var applyClassesEarly = maxDuration === 0
+ && isStructural
+ && addRemoveClassName.length > 0
+ && !flags.transitionClassBlock;
+
+ // this is an early check to avoid having to do another call to getComputedStyle
+ // call which is expensive. GCS calls are cached to speed things up.
+ if (!applyClassesEarly && maxDuration === 0 && !flags.recalculateTimingStyles) {
+ close();
+ return false;
+ }
+
+ if (applyClassesEarly) {
+ applyAnimationClasses(element, options);
+
+ // no need to calculate this anymore
+ flags.recalculateTimingStyles = false;
+
+ fullClassName = node.className + ' ' + setupClasses;
+ cacheKey = gcsHashFn(node, fullClassName);
+
+ timings = computeTimings(node, fullClassName, cacheKey);
+ relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+ }
+
+ if (maxDuration === 0 && !flags.recalculateTimingStyles) {
+ close();
+ return false;
+ }
+
+ // we need to recalculate the delay value since we used a pre-emptive negative
+ // delay value and the delay value is required for the final event checking. This
+ // property will ensure that this will happen after the RAF phase has passed.
+ if (timings.transitionDuration > 0) {
+ flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst;
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+ if (!options.skipBlocking) {
+ flags.blockTransition = timings.transitionDuration > 0;
+ flags.blockKeyframeAnimation = timings.animationDuration > 0 &&
+ stagger.animationDelay > 0 &&
+ stagger.animationDuration === 0;
+ }
+
+ if (flags.blockTransition) {
+ applyAnimationFromStyles(element, options);
+ } else {
+ blockTransitions(node, false);
+ }
+
+ applyBlocking(maxDuration);
+
+ // TODO(matsko): for 1.5 change this code to have an animator object for better debugging
+ return {
+ end: endFn,
+ start: function() {
+ if (animationClosed) return;
+
+ runnerHost = {
+ end: endFn,
+ cancel: cancelFn,
+ resume: null, //this will be set during the start() phase
+ pause: null
+ };
+
+ runner = new $$AnimateRunner(runnerHost);
+
+ waitUntilQuiet(start);
+
+ // we don't have access to pause/resume the animation
+ // since it hasn't run yet. AnimateRunner will therefore
+ // set noop functions for resume and pause and they will
+ // later be overridden once the animation is triggered
+ return runner;
+ }
+ };
+
+ function endFn() {
+ close();
+ }
+
+ function cancelFn() {
+ close(true);
+ }
+
+ function close(rejected) { // jshint ignore:line
+ // if the promise has been called already then we shouldn't close
+ // the animation again
+ if (animationClosed || (animationCompleted && animationPaused)) return;
+ animationClosed = true;
+ animationPaused = false;
+
+ $$jqLite.removeClass(element, setupClasses);
+ $$jqLite.removeClass(element, activeClasses);
+
+ blockKeyframeAnimations(node, false);
+ 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.
+ node.style[entry[0]] = '';
+ });
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+
+ // the reason why we have this option is to allow a synchronous closing callback
+ // that is fired as SOON as the animation ends (when the CSS is removed) or if
+ // the animation never takes off at all. A good example is a leave animation since
+ // the element must be removed just after the animation is over or else the element
+ // will appear on screen for one animation frame causing an overbearing flicker.
+ if (options.onDone) {
+ options.onDone();
+ }
+
+ // if the preparation function fails then the promise is not setup
+ if (runner) {
+ runner.complete(!rejected);
+ }
+ }
+
+ function applyBlocking(duration) {
+ if (flags.blockTransition) {
+ blockTransitions(node, duration);
+ }
+
+ if (flags.blockKeyframeAnimation) {
+ blockKeyframeAnimations(node, !!duration);
+ }
+ }
+
+ function start() {
+ if (animationClosed) return;
+
+ var startTime, events = [];
+
+ // even though we only pause keyframe animations here the pause flag
+ // will still happen when transitions are used. Only the transition will
+ // not be paused since that is not possible. If the animation ends when
+ // paused then it will not complete until unpaused or cancelled.
+ var playPause = function(playAnimation) {
+ if (!animationCompleted) {
+ animationPaused = !playAnimation;
+ if (timings.animationDuration) {
+ var value = blockKeyframeAnimations(node, animationPaused);
+ animationPaused
+ ? temporaryStyles.push(value)
+ : removeFromArray(temporaryStyles, value);
+ }
+ } else if (animationPaused && playAnimation) {
+ animationPaused = false;
+ close();
+ }
+ };
+
+ // checking the stagger duration prevents an accidently cascade of the CSS delay style
+ // being inherited from the parent. If the transition duration is zero then we can safely
+ // rely that the delay value is an intential stagger delay style.
+ var maxStagger = itemIndex > 0
+ && ((timings.transitionDuration && stagger.transitionDuration === 0) ||
+ (timings.animationDuration && stagger.animationDuration === 0))
+ && Math.max(stagger.animationDelay, stagger.transitionDelay);
+ if (maxStagger) {
+ $timeout(triggerAnimationStart,
+ Math.floor(maxStagger * itemIndex * ONE_SECOND),
+ false);
+ } else {
+ triggerAnimationStart();
+ }
+
+ // this will decorate the existing promise runner with pause/resume methods
+ runnerHost.resume = function() {
+ playPause(true);
+ };
+
+ runnerHost.pause = function() {
+ playPause(false);
+ };
+
+ function triggerAnimationStart() {
+ // just incase a stagger animation kicks in when the animation
+ // itself was cancelled entirely
+ if (animationClosed) return;
+
+ applyBlocking(false);
+
+ forEach(temporaryStyles, function(entry) {
+ var key = entry[0];
+ var value = entry[1];
+ node.style[key] = value;
+ });
+
+ applyAnimationClasses(element, options);
+ $$jqLite.addClass(element, activeClasses);
+
+ if (flags.recalculateTimingStyles) {
+ fullClassName = node.className + ' ' + setupClasses;
+ cacheKey = gcsHashFn(node, fullClassName);
+
+ timings = computeTimings(node, fullClassName, cacheKey);
+ relativeDelay = timings.maxDelay;
+ maxDelay = Math.max(relativeDelay, 0);
+ maxDuration = timings.maxDuration;
+
+ if (maxDuration === 0) {
+ close();
+ return;
+ }
+
+ flags.hasTransitions = timings.transitionDuration > 0;
+ flags.hasAnimations = timings.animationDuration > 0;
+ }
+
+ if (flags.applyTransitionDelay || flags.applyAnimationDelay) {
+ relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay)
+ ? parseFloat(options.delay)
+ : relativeDelay;
+
+ maxDelay = Math.max(relativeDelay, 0);
+
+ var delayStyle;
+ if (flags.applyTransitionDelay) {
+ timings.transitionDelay = relativeDelay;
+ delayStyle = getCssDelayStyle(relativeDelay);
+ temporaryStyles.push(delayStyle);
+ node.style[delayStyle[0]] = delayStyle[1];
+ }
+
+ if (flags.applyAnimationDelay) {
+ timings.animationDelay = relativeDelay;
+ delayStyle = getCssDelayStyle(relativeDelay, true);
+ temporaryStyles.push(delayStyle);
+ node.style[delayStyle[0]] = delayStyle[1];
+ }
+ }
+
+ maxDelayTime = maxDelay * ONE_SECOND;
+ maxDurationTime = maxDuration * ONE_SECOND;
+
+ if (options.easing) {
+ var easeProp, easeVal = options.easing;
+ if (flags.hasTransitions) {
+ easeProp = TRANSITION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ if (flags.hasAnimations) {
+ easeProp = ANIMATION_PROP + TIMING_KEY;
+ temporaryStyles.push([easeProp, easeVal]);
+ node.style[easeProp] = easeVal;
+ }
+ }
+
+ if (timings.transitionDuration) {
+ events.push(TRANSITIONEND_EVENT);
+ }
+
+ if (timings.animationDuration) {
+ events.push(ANIMATIONEND_EVENT);
+ }
+
+ startTime = Date.now();
+ element.on(events.join(' '), onAnimationProgress);
+ $timeout(onAnimationExpired, maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime);
+
+ applyAnimationToStyles(element, options);
+ }
+
+ function onAnimationExpired() {
+ // although an expired animation is a failed animation, getting to
+ // this outcome is very easy if the CSS code screws up. Therefore we
+ // should still continue normally as if the animation completed correctly.
+ close();
+ }
+
+ function onAnimationProgress(event) {
+ event.stopPropagation();
+ var ev = event.originalEvent || event;
+ var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
+
+ /* Firefox (or possibly just Gecko) likes to not round values up
+ * when a ms measurement is used for the animation */
+ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
+
+ /* $manualTimeStamp is a mocked timeStamp value which is set
+ * within browserTrigger(). This is only here so that tests can
+ * mock animations properly. Real events fallback to event.timeStamp,
+ * or, if they don't, then a timeStamp is automatically created for them.
+ * We're checking to see if the timeStamp surpasses the expected delay,
+ * but we're using elapsedTime instead of the timeStamp on the 2nd
+ * pre-condition since animations sometimes close off early */
+ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
+ // we set this flag to ensure that if the transition is paused then, when resumed,
+ // the animation will automatically close itself since transitions cannot be paused.
+ animationCompleted = true;
+ close();
+ }
+ }
+ }
+ }
+ }];
+}];
diff --git a/src/ngAnimate/animateCssDriver.js b/src/ngAnimate/animateCssDriver.js
new file mode 100644
index 000000000000..f0e2fdace984
--- /dev/null
+++ b/src/ngAnimate/animateCssDriver.js
@@ -0,0 +1,218 @@
+'use strict';
+
+var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) {
+ $$animationProvider.drivers.push('$$animateCssDriver');
+
+ var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
+ var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-animate-anchor';
+ var NG_ANIMATE_ANCHOR_SUFFIX = '-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) {
+
+ // only browsers that support these properties can render animations
+ if (!$sniffer.animations && !$sniffer.transitions) return noop;
+
+ var bodyNode = $document[0].body;
+ var rootNode = $rootElement[0];
+
+ var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode);
+
+ return function initDriverFn(animationDetails) {
+ return animationDetails.from && animationDetails.to
+ ? prepareFromToAnchorAnimation(animationDetails.from,
+ animationDetails.to,
+ animationDetails.classes,
+ animationDetails.anchors)
+ : prepareRegularAnimation(animationDetails);
+ };
+
+ function filterCssClasses(classes) {
+ //remove all the `ng-` stuff
+ return classes.replace(/\bng-\S+\b/g, '');
+ }
+
+ function getUniqueValues(a, b) {
+ if (isString(a)) a = a.split(' ');
+ if (isString(b)) b = b.split(' ');
+ return a.filter(function(val) {
+ return b.indexOf(val) === -1;
+ }).join(' ');
+ }
+
+ function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
+ var clone = jqLite(outAnchor[0].cloneNode(true));
+ var startingClasses = filterCssClasses(clone.attr('class') || '');
+ var anchorClasses = pendClasses(classes, NG_ANIMATE_ANCHOR_SUFFIX);
+
+ outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
+
+ clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME);
+ clone.addClass(anchorClasses);
+
+ rootBodyElement.append(clone);
+
+ var animatorOut = prepareOutAnimation();
+ if (!animatorOut) {
+ return end();
+ }
+
+ return {
+ start: function() {
+ var runner;
+
+ var currentAnimation = animatorOut.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ var animatorIn = prepareInAnimation();
+ if (animatorIn) {
+ currentAnimation = animatorIn.start();
+ currentAnimation.done(function() {
+ currentAnimation = null;
+ end();
+ runner.complete();
+ });
+ return currentAnimation;
+ }
+ // in the event that there is no `in` animation
+ end();
+ runner.complete();
+ });
+
+ runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn
+ });
+
+ return runner;
+
+ function endFn() {
+ if (currentAnimation) {
+ currentAnimation.end();
+ }
+ }
+ }
+ };
+
+ function calculateAnchorStyles(anchor) {
+ var styles = {};
+
+ var coords = anchor[0].getBoundingClientRect();
+
+ // we iterate directly since safari messes up and doesn't return
+ // all the keys for the coods object when iterated
+ forEach(['width','height','top','left'], function(key) {
+ var value = coords[key];
+ switch (key) {
+ case 'top':
+ value += bodyNode.scrollTop;
+ break;
+ case 'left':
+ value += bodyNode.scrollLeft;
+ break;
+ }
+ styles[key] = Math.floor(value) + 'px';
+ });
+ return styles;
+ }
+
+ function prepareOutAnimation() {
+ return $animateCss(clone, {
+ addClass: NG_OUT_ANCHOR_CLASS_NAME,
+ delay: true,
+ from: calculateAnchorStyles(outAnchor)
+ });
+ }
+
+ function prepareInAnimation() {
+ var endingClasses = filterCssClasses(inAnchor.attr('class'));
+ var classes = getUniqueValues(endingClasses, startingClasses);
+ return $animateCss(clone, {
+ to: calculateAnchorStyles(inAnchor),
+ addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + classes,
+ removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + startingClasses,
+ delay: true
+ });
+ }
+
+ function end() {
+ clone.remove();
+ outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
+ }
+ }
+
+ function prepareFromToAnchorAnimation(from, to, classes, anchors) {
+ var fromAnimation = prepareRegularAnimation(from);
+ var toAnimation = prepareRegularAnimation(to);
+
+ var anchorAnimations = [];
+ forEach(anchors, function(anchor) {
+ var outElement = anchor['out'];
+ var inElement = anchor['in'];
+ var animator = prepareAnchoredAnimation(classes, outElement, inElement);
+ if (animator) {
+ anchorAnimations.push(animator);
+ }
+ });
+
+ // no point in doing anything when there are no elements to animate
+ if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ forEach(anchorAnimations, function(animation) {
+ animationRunners.push(animation.start());
+ });
+
+ var runner = new $$AnimateRunner({
+ end: endFn,
+ cancel: endFn // CSS-driven animations cannot be cancelled, only ended
+ });
+
+ $$AnimateRunner.all(animationRunners, function(status) {
+ runner.complete(status);
+ });
+
+ return runner;
+
+ function endFn() {
+ forEach(animationRunners, function(runner) {
+ runner.end();
+ });
+ }
+ }
+ };
+ }
+
+ function prepareRegularAnimation(animationDetails) {
+ var element = animationDetails.element;
+ var options = animationDetails.options || {};
+ options.structural = animationDetails.structural;
+
+ // 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' && animationDetails.domOperation) {
+ options.onDone = animationDetails.domOperation;
+ }
+
+ return $animateCss(element, options);
+ }
+ }];
+}];
diff --git a/src/ngAnimate/animateJs.js b/src/ngAnimate/animateJs.js
new file mode 100644
index 000000000000..4395fbf9f81c
--- /dev/null
+++ b/src/ngAnimate/animateJs.js
@@ -0,0 +1,250 @@
+'use strict';
+
+// TODO(matsko): use caching here to speed things up for detection
+// TODO(matsko): add documentation
+// by the time...
+
+var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
+ this.$get = ['$injector', '$$AnimateRunner', '$$rAFMutex', '$$jqLite',
+ function($injector, $$AnimateRunner, $$rAFMutex, $$jqLite) {
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ // $animateJs(element, 'enter');
+ return function(element, event, classes, options) {
+ // the `classes` argument is optional and if it is not used
+ // then the classes will be resolved from the element's className
+ // property as well as options.addClass/options.removeClass.
+ if (arguments.length === 3 && isObject(classes)) {
+ options = classes;
+ classes = null;
+ }
+
+ options = prepareAnimationOptions(options);
+ if (!classes) {
+ classes = element.attr('class') || '';
+ if (options.addClass) {
+ classes += ' ' + options.addClass;
+ }
+ if (options.removeClass) {
+ classes += ' ' + options.removeClass;
+ }
+ }
+
+ var classesToAdd = options.addClass;
+ var classesToRemove = options.removeClass;
+
+ // the lookupAnimations function returns a series of animation objects that are
+ // matched up with one or more of the CSS classes. These animation objects are
+ // defined via the module.animation factory function. If nothing is detected then
+ // 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') {
+ beforeFn = 'leave';
+ afterFn = 'afterLeave'; // TODO(matsko): get rid of this
+ } else {
+ beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ afterFn = event;
+ }
+
+ if (event !== 'enter' && event !== 'move') {
+ before = packageAnimations(element, event, options, animations, beforeFn);
+ }
+ after = packageAnimations(element, event, options, animations, afterFn);
+ }
+
+ // no matching animations
+ if (!before && !after) return;
+
+ function applyOptions() {
+ options.domOperation();
+ applyAnimationClasses(element, options);
+ }
+
+ return {
+ start: function() {
+ var closeActiveAnimations;
+ var chain = [];
+
+ if (before) {
+ chain.push(function(fn) {
+ closeActiveAnimations = before(fn);
+ });
+ }
+
+ if (chain.length) {
+ chain.push(function(fn) {
+ applyOptions();
+ fn(true);
+ });
+ } else {
+ applyOptions();
+ }
+
+ if (after) {
+ chain.push(function(fn) {
+ closeActiveAnimations = after(fn);
+ });
+ }
+
+ var animationClosed = false;
+ var runner = new $$AnimateRunner({
+ end: function() {
+ endAnimations();
+ },
+ cancel: function() {
+ endAnimations(true);
+ }
+ });
+
+ $$AnimateRunner.chain(chain, onComplete);
+ return runner;
+
+ function onComplete(success) {
+ animationClosed = true;
+ applyOptions();
+ applyAnimationStyles(element, options);
+ runner.complete(success);
+ }
+
+ function endAnimations(cancelled) {
+ if (!animationClosed) {
+ (closeActiveAnimations || noop)(cancelled);
+ onComplete(cancelled);
+ }
+ }
+ }
+ };
+
+ function executeAnimationFn(fn, element, event, options, onDone) {
+ var args;
+ switch (event) {
+ case 'animate':
+ args = [element, options.from, options.to, onDone];
+ break;
+
+ case 'setClass':
+ args = [element, classesToAdd, classesToRemove, onDone];
+ break;
+
+ case 'addClass':
+ args = [element, classesToAdd, onDone];
+ break;
+
+ case 'removeClass':
+ args = [element, classesToRemove, onDone];
+ break;
+
+ default:
+ args = [element, onDone];
+ break;
+ }
+
+ args.push(options);
+
+ var value = fn.apply(fn, args);
+
+ // optional onEnd / onCancel callback
+ return isFunction(value) ? value : noop;
+ }
+
+ function groupEventedAnimations(element, event, options, animations, fnName) {
+ var operations = [];
+ forEach(animations, function(ani) {
+ var animation = ani[fnName];
+ if (!animation) return;
+
+ // note that all of these animations will run in parallel
+ operations.push(function() {
+ var runner;
+ var endProgressCb;
+
+ var resolved = false;
+ var onAnimationComplete = function(rejected) {
+ if (!resolved) {
+ resolved = true;
+ (endProgressCb || noop)(rejected);
+ runner.complete(!rejected);
+ }
+ };
+
+ runner = new $$AnimateRunner({
+ end: function() {
+ onAnimationComplete();
+ },
+ cancel: function() {
+ onAnimationComplete(true);
+ }
+ });
+
+ endProgressCb = executeAnimationFn(animation, element, event, options, function(result) {
+ var cancelled = result === false;
+ onAnimationComplete(cancelled);
+ });
+
+ return runner;
+ });
+ });
+
+ return operations;
+ }
+
+ function packageAnimations(element, event, options, animations, fnName) {
+ var operations = groupEventedAnimations(element, event, options, animations, fnName);
+ if (operations.length === 0) {
+ 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');
+ b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass');
+ }
+
+ if (a) {
+ operations = operations.concat(a);
+ }
+ if (b) {
+ operations = operations.concat(b);
+ }
+ }
+
+ if (operations.length === 0) return;
+
+ // TODO(matsko): add documentation
+ return function startAnimation(callback) {
+ var runners = [];
+ if (operations.length) {
+ forEach(operations, function(animateFn) {
+ runners.push(animateFn());
+ });
+ }
+
+ runners.length ? $$AnimateRunner.all(runners, callback) : callback();
+
+ return function endFn(reject) {
+ forEach(runners, function(runner) {
+ reject ? runner.cancel() : runner.end();
+ });
+ };
+ };
+ }
+ };
+
+ function lookupAnimations(classes) {
+ classes = isArray(classes) ? classes : classes.split(' ');
+ var matches = [], flagMap = {};
+ 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;
+ }
+ }
+ return matches;
+ }
+ }];
+}];
diff --git a/src/ngAnimate/animateJsDriver.js b/src/ngAnimate/animateJsDriver.js
new file mode 100644
index 000000000000..bba970456872
--- /dev/null
+++ b/src/ngAnimate/animateJsDriver.js
@@ -0,0 +1,61 @@
+'use strict';
+
+var $$AnimateJsDriverProvider = ['$$animationProvider', 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);
+ var toAnimation = prepareAnimation(animationDetails.to);
+ if (!fromAnimation && !toAnimation) return;
+
+ return {
+ start: function() {
+ var animationRunners = [];
+
+ if (fromAnimation) {
+ animationRunners.push(fromAnimation.start());
+ }
+
+ if (toAnimation) {
+ animationRunners.push(toAnimation.start());
+ }
+
+ $$AnimateRunner.all(animationRunners, done);
+
+ var runner = new $$AnimateRunner({
+ end: endFnFactory(),
+ cancel: endFnFactory()
+ });
+
+ return runner;
+
+ function endFnFactory() {
+ return function() {
+ forEach(animationRunners, function(runner) {
+ // at this point we cannot cancel animations for groups just yet. 1.5+
+ runner.end();
+ });
+ };
+ }
+
+ function done(status) {
+ runner.complete(status);
+ }
+ }
+ };
+ } else {
+ return prepareAnimation(animationDetails);
+ }
+ };
+
+ function prepareAnimation(animationDetails) {
+ // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations
+ var element = animationDetails.element;
+ var event = animationDetails.event;
+ var options = animationDetails.options;
+ var classes = animationDetails.classes;
+ return $$animateJs(element, event, classes, options);
+ }
+ }];
+}];
diff --git a/src/ngAnimate/animateQueue.js b/src/ngAnimate/animateQueue.js
new file mode 100644
index 000000000000..3d5b58f27fc5
--- /dev/null
+++ b/src/ngAnimate/animateQueue.js
@@ -0,0 +1,551 @@
+'use strict';
+
+var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
+var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
+ var PRE_DIGEST_STATE = 1;
+ var RUNNING_STATE = 2;
+
+ var rules = this.rules = {
+ skip: [],
+ cancel: [],
+ join: []
+ };
+
+ function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
+ return rules[ruleType].some(function(fn) {
+ return fn(element, currentAnimation, previousAnimation);
+ });
+ }
+
+ function hasAnimationClasses(options, and) {
+ options = options || {};
+ var a = (options.addClass || '').length > 0;
+ var b = (options.removeClass || '').length > 0;
+ return and ? a && b : a || b;
+ }
+
+ rules.join.push(function(element, newAnimation, currentAnimation) {
+ // if the new animation is class-based then we can just tack that on
+ return !newAnimation.structural && hasAnimationClasses(newAnimation.options);
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // there is no need to animate anything if no classes are being added and
+ // there is no structural animation that will be triggered
+ return !newAnimation.structural && !hasAnimationClasses(newAnimation.options);
+ });
+
+ rules.skip.push(function(element, 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;
+ });
+
+ rules.skip.push(function(element, newAnimation, currentAnimation) {
+ // if there is a current animation then skip the class-based animation
+ return currentAnimation.structural && !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;
+ });
+
+ rules.cancel.push(function(element, 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;
+ });
+
+ this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
+ '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite',
+ function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
+ $$animation, $$AnimateRunner, $templateRequest, $$jqLite) {
+
+ 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
+ // templates currently downloading then the watcher will still fire anyway.
+ var deregisterWatch = $rootScope.$watch(
+ function() { return $templateRequest.totalPendingRequests === 0; },
+ function(isEmpty) {
+ if (!isEmpty) return;
+ deregisterWatch();
+
+ // Now that all templates have been downloaded, $animate will wait until
+ // the post digest queue is empty before enabling animations. By having two
+ // calls to $postDigest calls we can ensure that the flag is enabled at the
+ // very end of the post digest queue. Since all of the animations in $animate
+ // use $postDigest, it's important that the code below executes at the end.
+ // This basically means that the page is fully downloaded and compiled before
+ // any animations are triggered.
+ $rootScope.$$postDigest(function() {
+ $rootScope.$$postDigest(function() {
+ // we check for null directly in the event that the application already called
+ // .enabled() with whatever arguments that it provided it with
+ if (animationsEnabled === null) {
+ animationsEnabled = true;
+ }
+ });
+ });
+ }
+ );
+
+ 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();
+ var isAnimatableClassName = !classNameFilter
+ ? function() { return true; }
+ : function(className) {
+ return classNameFilter.test(className);
+ };
+
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ function normalizeAnimationOptions(element, options) {
+ return mergeAnimationOptions(element, options, {});
+ }
+
+ function findCallbacks(element, event) {
+ var targetNode = element[0];
+
+ var matches = [];
+ var entries = callbackRegistry[event];
+ if (entries) {
+ forEach(entries, function(entry) {
+ if (entry.node.contains(targetNode)) {
+ matches.push(entry.callback);
+ }
+ });
+ }
+
+ return matches;
+ }
+
+ function triggerCallback(event, element, phase, data) {
+ $$rAF(function() {
+ forEach(findCallbacks(element, event), function(callback) {
+ callback(element, phase, data);
+ });
+ });
+ }
+
+ return {
+ on: function(event, container, callback) {
+ var node = extractElementNode(container);
+ callbackRegistry[event] = callbackRegistry[event] || [];
+ callbackRegistry[event].push({
+ node: node,
+ callback: callback
+ });
+ },
+
+ off: function(event, container, callback) {
+ var entries = callbackRegistry[event];
+ if (!entries) return;
+
+ callbackRegistry[event] = arguments.length === 1
+ ? null
+ : filterFromRegistry(entries, container, callback);
+
+ function filterFromRegistry(list, matchContainer, matchCallback) {
+ var containerNode = extractElementNode(matchContainer);
+ return list.filter(function(entry) {
+ var isMatch = entry.node === containerNode &&
+ (!matchCallback || entry.callback === matchCallback);
+ return !isMatch;
+ });
+ }
+ },
+
+ push: function(element, event, options, domOperation) {
+ options = options || {};
+ options.domOperation = domOperation;
+ return queueAnimation(element, event, options);
+ },
+
+ // this method has four signatures:
+ // () - global getter
+ // (bool) - global setter
+ // (element) - element getter
+ // (element, bool) - element setter
+ enabled: function(element, bool) {
+ var argCount = arguments.length;
+
+ if (argCount === 0) {
+ // () - Global getter
+ bool = !!animationsEnabled;
+ } else {
+ var hasElement = isElement(element);
+
+ if (!hasElement) {
+ // (bool) - Global setter
+ bool = animationsEnabled = !!element;
+ } else {
+ var node = element.length ? element[0] : element;
+ var recordExists = disabledElementsLookup.get(node);
+
+ if (argCount === 1) {
+ // (element) - Element getter
+ bool = !recordExists;
+ } else {
+ // (element, bool) - Element setter
+ bool = !!bool;
+ if (!bool) {
+ disabledElementsLookup.put(node, true);
+ } else if (recordExists) {
+ disabledElementsLookup.remove(node);
+ }
+ }
+ }
+ }
+
+ return bool;
+ }
+ };
+
+ function queueAnimation(element, event, options) {
+ element = stripCommentsFromElement(element);
+ var node = element[0];
+
+ options = prepareAnimationOptions(options);
+ var parent = element.parent();
+
+ // 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) {
+ runner.end();
+ return runner;
+ }
+
+ if (isArray(options.addClass)) {
+ options.addClass = options.addClass.join(' ');
+ }
+
+ if (isArray(options.removeClass)) {
+ options.removeClass = options.removeClass.join(' ');
+ }
+
+ if (options.from && !isObject(options.from)) {
+ options.from = null;
+ }
+
+ if (options.to && !isObject(options.to)) {
+ options.to = null;
+ }
+
+ var className = [node.className, options.addClass, options.removeClass].join(' ');
+ if (!isAnimatableClassName(className)) {
+ runner.end();
+ return runner;
+ }
+
+ var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
+
+ // this is a hard disable of all animations for the application or on
+ // the element itself, therefore there is no need to continue further
+ // past this point if not enabled
+ var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node);
+ 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) {
+ close();
+ return runner;
+ }
+
+ if (isStructural) {
+ closeChildAnimations(element);
+ }
+
+ var newAnimation = {
+ structural: isStructural,
+ element: element,
+ event: event,
+ options: options,
+ runner: runner
+ };
+
+ if (hasExistingAnimation) {
+ var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
+ if (skipAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ close();
+ return runner;
+ } else {
+ mergeAnimationOptions(element, existingAnimation.options, options);
+ return existingAnimation.runner;
+ }
+ }
+
+ var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
+ if (cancelAnimationFlag) {
+ if (existingAnimation.state === RUNNING_STATE) {
+ existingAnimation.runner.end();
+ } else {
+ mergeAnimationOptions(element, newAnimation.options, existingAnimation.options);
+ }
+ } 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 {
+ event = newAnimation.event = existingAnimation.event;
+ options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
+ return runner;
+ }
+ }
+ }
+ } else {
+ // normalization in this case means that it removes redundant CSS classes that
+ // already exist (addClass) or do not exist (removeClass) on the element
+ normalizeAnimationOptions(element, options);
+ }
+
+ // when the options are merged and cleaned up we may end up not having to do
+ // an animation at all, therefore we should check this before issuing a post
+ // digest callback. Structural animations will always run no matter what.
+ var isValidAnimation = newAnimation.structural;
+ if (!isValidAnimation) {
+ // animate (from/to) can be quickly checked first, otherwise we check if any classes are present
+ isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
+ || hasAnimationClasses(newAnimation.options);
+ }
+
+ if (!isValidAnimation) {
+ close();
+ return runner;
+ }
+
+ closeParentClassBasedAnimations(parent);
+
+ // the counter keeps track of cancelled animations
+ var counter = (existingAnimation.counter || 0) + 1;
+ newAnimation.counter = counter;
+
+ markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
+
+ $rootScope.$$postDigest(function() {
+ var animationDetails = activeAnimationsLookup.get(node);
+ var animationCancelled = !animationDetails;
+ animationDetails = animationDetails || {};
+
+ // if addClass/removeClass is called before something like enter then the
+ // registered parent element may not be present. The code below will ensure
+ // that a final value for parent element is obtained
+ var parentElement = element.parent() || [];
+
+ // animate/structural/class-based animations all have requirements. Otherwise there
+ // is no point in performing an animation. The parent node must also be set.
+ var isValidAnimation = parentElement.length > 0
+ && (animationDetails.event === 'animate'
+ || animationDetails.structural
+ || hasAnimationClasses(animationDetails.options));
+
+ // this means that the previous animation was cancelled
+ // even if the follow-up animation is the same event
+ if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
+ // if another animation did not take over then we need
+ // to make sure that the domOperation and options are
+ // handled accordingly
+ if (animationCancelled) {
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ }
+
+ // if the event changed from something like enter to leave then we do
+ // it, otherwise if it's the same then the end result will be the same too
+ if (animationCancelled || (isStructural && animationDetails.event !== event)) {
+ options.domOperation();
+ }
+
+ return;
+ }
+
+ // this combined multiple class to addClass / removeClass into a setClass event
+ // so long as a structural event did not take over the animation
+ event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true)
+ ? 'setClass'
+ : animationDetails.event;
+
+ closeParentClassBasedAnimations(parentElement);
+
+ markElementAnimationState(element, RUNNING_STATE);
+ var realRunner = $$animation(element, event, animationDetails.options);
+ realRunner.done(function(status) {
+ close(!status);
+ var animationDetails = activeAnimationsLookup.get(node);
+ if (animationDetails && animationDetails.counter === counter) {
+ clearElementAnimationState(element);
+ }
+ notifyProgress(runner, event, 'close', {});
+ });
+
+ // this will update the runner's flow-control events based on
+ // the `realRunner` object.
+ runner.setHost(realRunner);
+ notifyProgress(runner, event, 'start', {});
+ });
+
+ return runner;
+
+ function notifyProgress(runner, event, phase, data) {
+ triggerCallback(event, element, phase, data);
+ runner.progress(event, phase, data);
+ }
+
+ function close(reject) { // jshint ignore:line
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+ runner.complete(!reject);
+ }
+ }
+
+ function closeChildAnimations(element) {
+ var node = element[0];
+ var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
+ forEach(children, function(child) {
+ var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
+ var animationDetails = activeAnimationsLookup.get(child);
+ switch (state) {
+ case RUNNING_STATE:
+ animationDetails.runner.end();
+ /* falls through */
+ case PRE_DIGEST_STATE:
+ if (animationDetails) {
+ activeAnimationsLookup.remove(child);
+ }
+ break;
+ }
+ });
+ }
+
+ function clearElementAnimationState(element) {
+ element = element.length ? element[0] : element;
+ element.removeAttribute(NG_ANIMATE_ATTR_NAME);
+ activeAnimationsLookup.remove(element);
+ }
+
+ function isMatchingElement(a,b) {
+ a = a.length ? a[0] : a;
+ b = b.length ? b[0] : b;
+ return a === b;
+ }
+
+ function closeParentClassBasedAnimations(startingElement) {
+ var parentNode = startingElement[0];
+ 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) return;
+
+ if (animationDetails.state === RUNNING_STATE) {
+ animationDetails.runner.end();
+ }
+ clearElementAnimationState(node);
+ }
+ }
+
+ function areAnimationsAllowed(element, parent, event) {
+ var bodyElementDetected = false;
+ var rootElementDetected = false;
+ var parentAnimationDetected = false;
+ var animateChildren;
+
+ while (parent && parent.length) {
+ var parentNode = parent[0];
+ if (parentNode.nodeType !== ELEMENT_NODE) {
+ // no point in inspecting the #document element
+ break;
+ }
+
+ 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) {
+ parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode);
+ }
+
+ if (isUndefined(animateChildren) || animateChildren === true) {
+ var value = parent.data(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 (!rootElementDetected) {
+ // angular doesn't want to attempt to animate elements outside of the application
+ // therefore we need to ensure that the rootElement is an ancestor of the current element
+ rootElementDetected = isMatchingElement(parent, $rootElement);
+ }
+
+ 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(parent, bodyElement);
+ }
+
+ parent = parent.parent();
+ }
+
+ var allowAnimation = !parentAnimationDetected || animateChildren;
+ return allowAnimation && rootElementDetected && bodyElementDetected;
+ }
+
+ function markElementAnimationState(element, state, details) {
+ details = details || {};
+ details.state = state;
+
+ element = element.length ? element[0] : element;
+ element.setAttribute(NG_ANIMATE_ATTR_NAME, state);
+
+ var oldValue = activeAnimationsLookup.get(element);
+ var newValue = oldValue
+ ? extend(oldValue, details)
+ : details;
+ activeAnimationsLookup.put(element, newValue);
+ }
+ }];
+}];
diff --git a/src/ngAnimate/animateRunner.js b/src/ngAnimate/animateRunner.js
new file mode 100644
index 000000000000..65b1e8d48030
--- /dev/null
+++ b/src/ngAnimate/animateRunner.js
@@ -0,0 +1,151 @@
+'use strict';
+
+var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
+ return function() {
+ var passed = false;
+ $$rAF(function() {
+ passed = true;
+ });
+ return function(fn) {
+ passed ? fn() : $$rAF(fn);
+ };
+ };
+}];
+
+var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ AnimateRunner.chain = function(chain, callback) {
+ var index = 0;
+
+ next();
+ function next() {
+ if (index === chain.length) {
+ callback(true);
+ return;
+ }
+
+ chain[index](function(response) {
+ if (response === false) {
+ callback(false);
+ return;
+ }
+ index++;
+ next();
+ });
+ }
+ };
+
+ AnimateRunner.all = function(runners, callback) {
+ var count = 0;
+ var status = true;
+ forEach(runners, function(runner) {
+ runner.done(onProgress);
+ });
+
+ function onProgress(response) {
+ status = status && response;
+ if (++count === runners.length) {
+ callback(status);
+ }
+ }
+ };
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ this._doneCallbacks = [];
+ this._runInAnimationFrame = $$rAFMutex();
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._runInAnimationFrame(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+}];
diff --git a/src/ngAnimate/animation.js b/src/ngAnimate/animation.js
new file mode 100644
index 000000000000..0c52760d56bd
--- /dev/null
+++ b/src/ngAnimate/animation.js
@@ -0,0 +1,288 @@
+'use strict';
+
+var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
+ var NG_ANIMATE_CLASSNAME = 'ng-animate';
+ var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
+
+ var drivers = this.drivers = [];
+
+ var RUNNER_STORAGE_KEY = '$$animationRunner';
+
+ function setRunner(element, runner) {
+ element.data(RUNNER_STORAGE_KEY, runner);
+ }
+
+ function removeRunner(element) {
+ element.removeData(RUNNER_STORAGE_KEY);
+ }
+
+ function getRunner(element) {
+ return element.data(RUNNER_STORAGE_KEY);
+ }
+
+ this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner',
+ function($$jqLite, $rootScope, $injector, $$AnimateRunner) {
+
+ var animationQueue = [];
+ var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+
+ // TODO(matsko): document the signature in a better way
+ return function(element, event, options) {
+ 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
+ // methods leading into the driver's end/cancel methods
+ // for now they just stop the animation from starting
+ var runner = new $$AnimateRunner({
+ end: function() { close(); },
+ cancel: function() { close(true); }
+ });
+
+ 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;
+ }
+
+ animationQueue.push({
+ // this data is used by the postDigest code and passed into
+ // the driver step function
+ element: element,
+ classes: classes,
+ event: event,
+ structural: isStructural,
+ options: options,
+ start: start,
+ close: close
+ });
+
+ element.on('$destroy', handleDestroyedElement);
+
+ // we only want there to be one function called within the post digest
+ // 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() {
+ 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)) {
+ animations.push(entry);
+ }
+ });
+
+ // now any future animations will be in another postDigest
+ animationQueue.length = 0;
+
+ forEach(groupAnimations(animations), function(animationEntry) {
+ var startFn = animationEntry.start;
+ var closeFn = animationEntry.close;
+ var operation = invokeFirstDriver(animationEntry);
+ var startAnimation = operation && operation.start; /// TODO(matsko): only recognize operation.start()
+ if (!startAnimation) {
+ closeFn();
+ } else {
+ startFn();
+ var animationRunner = startAnimation();
+ animationRunner.done(function(status) {
+ closeFn(!status);
+ });
+ updateAnimationRunners(animationEntry, animationRunner);
+ }
+ });
+ });
+
+ return runner;
+
+ // TODO(matsko): change to reference nodes
+ function getAnchorNodes(node) {
+ var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']';
+ var items = node.hasAttribute(NG_ANIMATE_REF_ATTR)
+ ? [node]
+ : node.querySelectorAll(SELECTOR);
+ var anchors = [];
+ forEach(items, function(node) {
+ var attr = node.getAttribute(NG_ANIMATE_REF_ATTR);
+ if (attr && attr.length) {
+ anchors.push(node);
+ }
+ });
+ return anchors;
+ }
+
+ function groupAnimations(animations) {
+ var preparedAnimations = [];
+ var refLookup = {};
+ forEach(animations, function(animation, index) {
+ var element = animation.element;
+ var node = element[0];
+ var event = animation.event;
+ var enterOrMove = ['enter', 'move'].indexOf(event) >= 0;
+ var anchorNodes = animation.structural ? getAnchorNodes(node) : [];
+
+ if (anchorNodes.length) {
+ var direction = enterOrMove ? 'to' : 'from';
+
+ forEach(anchorNodes, function(anchor) {
+ var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR);
+ refLookup[key] = refLookup[key] || {};
+ refLookup[key][direction] = {
+ animationID: index,
+ element: jqLite(anchor)
+ };
+ });
+ } else {
+ preparedAnimations.push(animation);
+ }
+ });
+
+ var usedIndicesLookup = {};
+ var anchorGroups = {};
+ forEach(refLookup, function(operations, key) {
+ var from = operations.from;
+ var to = operations.to;
+
+ if (!from || !to) {
+ // only one of these is set therefore we can't have an
+ // anchor animation since all three pieces are required
+ var index = from ? from.animationID : to.animationID;
+ var indexKey = index.toString();
+ if (!usedIndicesLookup[indexKey]) {
+ usedIndicesLookup[indexKey] = true;
+ preparedAnimations.push(animations[index]);
+ }
+ return;
+ }
+
+ var fromAnimation = animations[from.animationID];
+ var toAnimation = animations[to.animationID];
+ var lookupKey = from.animationID.toString();
+ if (!anchorGroups[lookupKey]) {
+ var group = anchorGroups[lookupKey] = {
+ // TODO(matsko): double-check this code
+ start: function() {
+ fromAnimation.start();
+ toAnimation.start();
+ },
+ close: function() {
+ fromAnimation.close();
+ toAnimation.close();
+ },
+ classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes),
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [] // TODO(matsko): change to reference nodes
+ };
+
+ // the anchor animations require that the from and to elements both have at least
+ // one shared CSS class which effictively marries the two elements together to use
+ // the same animation driver and to properly sequence the anchor animation.
+ if (group.classes.length) {
+ preparedAnimations.push(group);
+ } else {
+ preparedAnimations.push(fromAnimation);
+ preparedAnimations.push(toAnimation);
+ }
+ }
+
+ anchorGroups[lookupKey].anchors.push({
+ 'out': from.element, 'in': to.element
+ });
+ });
+
+ return preparedAnimations;
+ }
+
+ function cssClassesIntersection(a,b) {
+ a = a.split(' ');
+ b = b.split(' ');
+ var matches = [];
+
+ for (var i = 0; i < a.length; i++) {
+ var aa = a[i];
+ if (aa.substring(0,3) === 'ng-') continue;
+
+ for (var j = 0; j < b.length; j++) {
+ if (aa === b[j]) {
+ matches.push(aa);
+ break;
+ }
+ }
+ }
+
+ return matches.join(' ');
+ }
+
+ 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 start() {
+ element.addClass(NG_ANIMATE_CLASSNAME);
+ if (tempClasses) {
+ $$jqLite.addClass(element, tempClasses);
+ }
+ }
+
+ function updateAnimationRunners(animation, newRunner) {
+ if (animation.from && animation.to) {
+ update(animation.from.element);
+ update(animation.to.element);
+ } else {
+ update(animation.element);
+ }
+
+ function update(element) {
+ getRunner(element).setHost(newRunner);
+ }
+ }
+
+ function handleDestroyedElement() {
+ var runner = getRunner(element);
+ if (runner && (event !== 'leave' || !options.$$domOperationFired)) {
+ runner.end();
+ }
+ }
+
+ function close(rejected) { // jshint ignore:line
+ element.off('$destroy', handleDestroyedElement);
+ removeRunner(element);
+
+ applyAnimationClasses(element, options);
+ applyAnimationStyles(element, options);
+ options.domOperation();
+
+ if (tempClasses) {
+ $$jqLite.removeClass(element, tempClasses);
+ }
+
+ element.removeClass(NG_ANIMATE_CLASSNAME);
+ runner.complete(!rejected);
+ }
+ };
+ }];
+}];
diff --git a/src/ngAnimate/module.js b/src/ngAnimate/module.js
new file mode 100644
index 000000000000..f5e90a9fd7b2
--- /dev/null
+++ b/src/ngAnimate/module.js
@@ -0,0 +1,508 @@
+'use strict';
+
+/* global angularAnimateModule: true,
+
+ $$rAFMutexFactory,
+ $$AnimateChildrenDirective,
+ $$AnimateRunnerFactory,
+ $$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` then the animation hooks are enabled for an Angular app.
+ *
+ *
+ *
+ * # 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 |
+ *
+ * (More information can be found by visiting each the documentation associated with each directive.)
+ *
+ * ## 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.
+ *
+ * The example below shows how an `enter` animation can be made possible on a element using `ng-if`:
+ *
+ * ```html
+ *
+ * Fade me in out
+ *
+ *
+ *
+ * ```
+ *
+ * Notice the CSS class **fade**? We can now create the CSS transition code that references this class:
+ *
+ * ```css
+ * /* The starting CSS styles for the enter animation */
+ * .fade.ng-enter {
+ * transition:0.5s linear all;
+ * opacity:0;
+ * }
+ *
+ * /* The starting CSS styles for the enter animation */
+ * .fade.ng-enter.ng-enter-active {
+ * opacity:1;
+ * }
+ * ```
+ *
+ * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two
+ * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition
+ * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards.
+ *
+ * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions:
+ *
+ * ```css
+ * /* now the element will fade out before it is removed from the DOM */
+ * .fade.ng-leave {
+ * transition:0.5s linear all;
+ * opacity:1;
+ * }
+ * .fade.ng-leave.ng-leave-active {
+ * opacity:0;
+ * }
+ * ```
+ *
+ * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class:
+ *
+ * ```css
+ * /* there is no need to define anything inside of the destination
+ * CSS class since the keyframe will take charge of the animation */
+ * .fade.ng-leave {
+ * animation: my_fade_animation 0.5s linear;
+ * -webkit-animation: my_fade_animation 0.5s linear;
+ * }
+ *
+ * @keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ *
+ * @-webkit-keyframes my_fade_animation {
+ * from { opacity:1; }
+ * to { opacity:0; }
+ * }
+ * ```
+ *
+ * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element.
+ *
+ * ### CSS Class-based Animations
+ *
+ * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different
+ * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added
+ * and removed.
+ *
+ * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class:
+ *
+ * ```html
+ *
+ * Show and hide me
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since
+ * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest.
+ *
+ * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation
+ * with CSS styles.
+ *
+ * ```html
+ *
+ * Highlight this box
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * We can also make use of CSS keyframes by placing them within the CSS classes.
+ *
+ *
+ * ### CSS Staggering Animations
+ * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
+ * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
+ * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
+ * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
+ * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
+ *
+ * ```css
+ * .my-animation.ng-enter {
+ * /* standard transition code */
+ * transition: 1s linear all;
+ * opacity:0;
+ * }
+ * .my-animation.ng-enter-stagger {
+ * /* this will have a 100ms delay between each successive leave animation */
+ * transition-delay: 0.1s;
+ *
+ * /* in case the stagger doesn't work then the duration value
+ * must be set to 0 to avoid an accidental CSS inheritance */
+ * transition-duration: 0s;
+ * }
+ * .my-animation.ng-enter.ng-enter-active {
+ * /* standard transition styles */
+ * opacity:1;
+ * }
+ * ```
+ *
+ * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
+ * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
+ * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
+ * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired.
+ *
+ * The following code will issue the **ng-leave-stagger** event on the element provided:
+ *
+ * ```js
+ * var kids = parent.children();
+ *
+ * $animate.leave(kids[0]); //stagger index=0
+ * $animate.leave(kids[1]); //stagger index=1
+ * $animate.leave(kids[2]); //stagger index=2
+ * $animate.leave(kids[3]); //stagger index=3
+ * $animate.leave(kids[4]); //stagger index=4
+ *
+ * window.requestAnimationFrame(function() {
+ * //stagger has reset itself
+ * $animate.leave(kids[5]); //stagger index=0
+ * $animate.leave(kids[6]); //stagger index=1
+ *
+ * $scope.$digest();
+ * });
+ * ```
+ *
+ * Stagger animations are currently only supported within CSS-defined animations.
+ *
+ * ## 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 ainmation.
+ *
+ * Let's see an example of a enter/leave animation using `ngRepeat`:
+ *
+ * ```html
+ *
+ * {{ item }}
+ *
+ * ```
+ *
+ * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`:
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * // 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
+ * // knows that the animation has concluded
+ * },
+ *
+ * move: function(element, doneFn) {
+ * jQuery(element).fadeIn(1000, doneFn);
+ * },
+ *
+ * leave: function(element, doneFn) {
+ * jQuery(element).fadeOut(1000, doneFn);
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
+ * greensock.js and velocity.js.
+ *
+ * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define
+ * our animations inside of the same registered animation, however, the function input arguments are a bit different:
+ *
+ * ```html
+ *
+ * this box is moody
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.colorful', [function() {
+ * return {
+ * addClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * removeClass: function(element, className, doneFn) {
+ * // do some cool animation and call the doneFn
+ * },
+ * setClass: function(element, addedClass, removedClass, doneFn) {
+ * // do some cool animation and call the doneFn
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * ## 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,
+ * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore example below will only result in **JS animations taking
+ * charge of the animation**:
+ *
+ * ```html
+ *
+ * Slide in and out
+ *
+ * ```
+ *
+ * ```js
+ * myModule.animation('.slide', [function() {
+ * return {
+ * enter: function(element, doneFn) {
+ * jQuery(element).slideIn(1000, doneFn);
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * ```css
+ * .slide.ng-enter {
+ * transition:0.5s linear all;
+ * transform:translateY(-100px);
+ * }
+ * .slide.ng-enter.ng-enter-active {
+ * transform:translateY(0);
+ * }
+ * ```
+ *
+ * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can suppliment for the
+ * lack of CSS animations by making use of the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from
+ * our own JS-based animation code:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var animation = $animateCss(element, {
+ * event: 'enter'
+ * });
+ *
+ * if (animation) {
+ * // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
+ * var runner = animation.start();
+ * runner.done(doneFn);
+ * } else { //no CSS animation was detected
+ * doneFn();
+ * }
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework.
+ *
+ * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or
+ * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that
+ * data into `$animateCss` directly:
+ *
+ * ```js
+ * myModule.animation('.slide', ['$animateCss', function($animateCss) {
+ * return {
+ * enter: function(element, doneFn) {
+ * var animation = $animateCss(element, {
+ * event: 'enter',
+ * addClass: 'maroon-setting',
+ * from: { height:0 },
+ * to: { height: 200 }
+ * });
+ *
+ * if (animation) {
+ * animation.start().done(doneFn);
+ * } else {
+ * doneFn();
+ * }
+ * }
+ * }
+ * }]
+ * ```
+ *
+ * Now we can fill in the rest via our transition CSS code:
+ *
+ * ```css
+ * /* the transition tells ngAnimate to make the animation happen */
+ * .slide.ng-enter { transition:0.5s linear all; }
+ *
+ * /* this extra CSS class will be absorbed into the transition
+ * since the $animateCss code is adding the class */
+ * .maroon-setting { background:red; }
+ * ```
+ *
+ * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over.
+ *
+ * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}.
+ *
+ *
+ * ## 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?
+ * 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
+ * Hi there
+ * ```
+ *
+ * ```js
+ * ngModule.directive('greetingBox', ['$animate', function($animate) {
+ * return function(scope, element, attrs) {
+ * attrs.$observe('active', function(value) {
+ * value ? $animate.addClass(element, 'on') ? $animate.removeClass(element, 'on');
+ * });
+ * });
+ * }]);
+ * ```
+ *
+ * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element
+ * in our HTML code then we can trigger a CSS or JS animation to happen.
+ *
+ * ```css
+ * /* normally we would create a CSS class to reference on the element */
+ * [greeting-box].on { transition:0.5s linear all; background:green; color:white; }
+ * ```
+ *
+ * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's
+ * possible be sure to visit the {@link ng.$animate $animate service API page}.
+ *
+ *
+ * ### Preventing Collisions With Third Party Libraries
+ *
+ * Some third-party frameworks place animation duration defaults across many element or className
+ * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which
+ * is expecting actual animations on these elements and has to wait for their completion.
+ *
+ * You can prevent this unwanted behavior by using a prefix on all your animation classes:
+ *
+ * ```css
+ * /* prefixed with animate- */
+ * .animate-fade-add.animate-fade-add-active {
+ * transition:1s linear all;
+ * opacity:0;
+ * }
+ * ```
+ *
+ * You then configure `$animate` to enforce this prefix:
+ *
+ * ```js
+ * $animateProvider.classNameFilter(/animate-/);
+ * ```
+ *
+ * This also may provide your application with a speed boost since only specific elements containing CSS class prefix
+ * will be evaluated for animation when any DOM changes occur in the application.
+ *
+ * ## Callbacks and Promises
+ *
+ * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger
+ * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has
+ * ended by chaining onto the returned promise that animation method returns.
+ *
+ * ```js
+ * // somewhere within the depths of the directive
+ * $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
+ * 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 `ng-view` element and we wanted our
+ * routing controller to hook into that:
+ *
+ * ```js
+ * ngModule.controller('HomePageController', ['$animate', function($animate) {
+ * $animate.on('enter', '[ng-view]', function(element) {
+ * // 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.)
+ */
+
+/**
+ * @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 $animate to learn more about animations with `$animate`}.
+ */
+angular.module('ngAnimate', [])
+ .directive('ngAnimateChildren', $$AnimateChildrenDirective)
+
+ .factory('$$rAFMutex', $$rAFMutexFactory)
+
+ .factory('$$AnimateRunner', $$AnimateRunnerFactory)
+
+ .provider('$$animateQueue', $$AnimateQueueProvider)
+ .provider('$$animation', $$AnimationProvider)
+
+ .provider('$animateCss', $AnimateCssProvider)
+ .provider('$$animateCssDriver', $$AnimateCssDriverProvider)
+
+ .provider('$$animateJs', $$AnimateJsProvider)
+ .provider('$$animateJsDriver', $$AnimateJsDriverProvider);
diff --git a/src/ngAnimate/shared.js b/src/ngAnimate/shared.js
new file mode 100644
index 000000000000..985d4d170109
--- /dev/null
+++ b/src/ngAnimate/shared.js
@@ -0,0 +1,230 @@
+'use strict';
+
+/* jshint ignore:start */
+var noop = angular.noop;
+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 NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren';
+
+var isPromiseLike = function(p) {
+ return p && p.then ? true : false;
+}
+
+function mergeClasses(a,b) {
+ if (!a && !b) return '';
+ if (!a) return b;
+ if (!b) return a;
+ if (isArray(a)) a = a.join(' ');
+ if (isArray(b)) b = b.join(' ');
+ return a + ' ' + b;
+}
+
+function packageStyles(options) {
+ var styles = {};
+ if (options && (options.to || options.from)) {
+ styles.to = options.to;
+ styles.from = options.from;
+ }
+ return styles;
+}
+
+function pendClasses(classes, fix, isPrefix) {
+ var className = '';
+ classes = isArray(classes)
+ ? classes
+ : classes && isString(classes) && classes.length
+ ? classes.split(/\s+/)
+ : [];
+ forEach(classes, function(klass, i) {
+ if (klass && klass.length > 0) {
+ className += (i > 0) ? ' ' : '';
+ className += isPrefix ? fix + klass
+ : klass + fix;
+ }
+ });
+ return className;
+}
+
+function removeFromArray(arr, val) {
+ var index = arr.indexOf(val);
+ if (val >= 0) {
+ arr.splice(index, 1);
+ }
+}
+
+function stripCommentsFromElement(element) {
+ if (element.nodeType === ELEMENT_NODE) {
+ return jqLite(element);
+ }
+ if (element.length === 0) return [];
+
+ // 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.)
+ if (element.length === 1) {
+ return element[0].nodeType === ELEMENT_NODE && element;
+ } else {
+ return jqLite(extractElementNode(element));
+ }
+}
+
+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) {
+ return elm;
+ }
+ }
+}
+
+function $$addClass($$jqLite, element, className) {
+ forEach(element, function(elm) {
+ $$jqLite.addClass(elm, className);
+ });
+}
+
+function $$removeClass($$jqLite, element, className) {
+ forEach(element, function(elm) {
+ $$jqLite.removeClass(elm, className);
+ });
+}
+
+function applyAnimationClassesFactory($$jqLite) {
+ return function(element, options) {
+ if (options.addClass) {
+ $$addClass($$jqLite, element, options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ $$removeClass($$jqLite, element, options.removeClass);
+ element.removeClass(options.removeClass);
+ options.removeClass = null;
+ }
+ }
+}
+
+function prepareAnimationOptions(options) {
+ options = options || {};
+ if (!options.$$prepared) {
+ var domOperation = options.domOperation || noop;
+ options.domOperation = function() {
+ options.$$domOperationFired = true;
+ domOperation();
+ domOperation = noop;
+ };
+ options.$$prepared = true;
+ }
+ return options;
+}
+
+function applyAnimationStyles(element, options) {
+ applyAnimationFromStyles(element, options);
+ applyAnimationToStyles(element, options);
+}
+
+function applyAnimationFromStyles(element, options) {
+ if (options.from) {
+ element.css(options.from);
+ options.from = null;
+ }
+}
+
+function applyAnimationToStyles(element, options) {
+ if (options.to) {
+ element.css(options.to);
+ options.to = null;
+ }
+}
+
+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);
+
+ extend(target, newOptions);
+
+ if (classes.addClass) {
+ target.addClass = classes.addClass;
+ } else {
+ target.addClass = null;
+ }
+
+ if (classes.removeClass) {
+ target.removeClass = classes.removeClass;
+ } else {
+ target.removeClass = null;
+ }
+
+ return target;
+}
+
+function resolveElementClasses(existing, toAdd, toRemove) {
+ var ADD_CLASS = 1;
+ var REMOVE_CLASS = -1;
+
+ var flags = {};
+ existing = splitClassesToLookup(existing);
+
+ toAdd = splitClassesToLookup(toAdd);
+ forEach(toAdd, function(value, key) {
+ flags[key] = ADD_CLASS;
+ });
+
+ toRemove = splitClassesToLookup(toRemove);
+ forEach(toRemove, function(value, key) {
+ flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS;
+ });
+
+ var classes = {
+ addClass: '',
+ removeClass: ''
+ };
+
+ forEach(flags, function(val, klass) {
+ var prop, allow;
+ if (val === ADD_CLASS) {
+ prop = 'addClass';
+ allow = !existing[klass];
+ } else if (val === REMOVE_CLASS) {
+ prop = 'removeClass';
+ allow = existing[klass];
+ }
+ if (allow) {
+ if (classes[prop].length) {
+ classes[prop] += ' ';
+ }
+ classes[prop] += klass;
+ }
+ });
+
+ function splitClassesToLookup(classes) {
+ if (isString(classes)) {
+ classes = classes.split(' ');
+ }
+
+ var obj = {};
+ forEach(classes, function(klass) {
+ // sometimes the split leaves empty string values
+ // incase extra spaces were applied to the options
+ if (klass.length) {
+ obj[klass] = true;
+ }
+ });
+ return obj;
+ }
+
+ return classes;
+}
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 6208a9fdb4cc..dd11e45701ab 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -764,13 +764,14 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
};
});
- $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser',
- function($delegate, $$asyncCallback, $timeout, $browser) {
+ $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', '$$rAF',
+ function($delegate, $$asyncCallback, $timeout, $browser, $$rAF) {
var animate = {
queue: [],
cancel: $delegate.cancel,
enabled: $delegate.enabled,
triggerCallbackEvents: function() {
+ $$rAF.flush();
$$asyncCallback.flush();
},
triggerCallbackPromise: function() {
@@ -1757,7 +1758,7 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
queue[i]();
}
- queue = [];
+ queue = queue.slice(i);
};
return rafFn;
diff --git a/test/.jshintrc b/test/.jshintrc
index 18ef9d956730..0d85795b3545 100644
--- a/test/.jshintrc
+++ b/test/.jshintrc
@@ -146,6 +146,7 @@
"they": false,
"tthey": false,
"xthey": false,
+ "assertCompareNodes": false,
/* e2e */
"browser": false,
@@ -165,6 +166,7 @@
"provideLog": false,
"spyOnlyCallsWithArgs": false,
"createMockStyleSheet": false,
+ "browserSupportsCssAnimations": false,
"browserTrigger": false,
"jqLiteCacheSize": false
}
diff --git a/test/helpers/privateMocks.js b/test/helpers/privateMocks.js
index 521b0c3abde5..f733afb8393f 100644
--- a/test/helpers/privateMocks.js
+++ b/test/helpers/privateMocks.js
@@ -1,6 +1,11 @@
'use strict';
/* globals xit */
+function assertCompareNodes(a,b,not) {
+ a = a[0] ? a[0] : a;
+ b = b[0] ? b[0] : b;
+ expect(a === b).toBe(!not);
+}
function baseThey(msg, vals, spec, itFn) {
var valsIsArray = angular.isArray(vals);
@@ -26,7 +31,14 @@ function xthey(msg, vals, spec) {
baseThey(msg, vals, spec, xit);
}
-
+function browserSupportsCssAnimations() {
+ var nav = window.navigator.appVersion;
+ if (nav.indexOf('MSIE') >= 0) {
+ var version = parseInt(navigator.appVersion.match(/MSIE ([\d.]+)/)[1]);
+ return version >= 10; //only IE10+ support keyframes / transitions
+ }
+ return true;
+}
function createMockStyleSheet(doc, wind) {
doc = doc ? doc[0] : document;
diff --git a/test/helpers/testabilityPatch.js b/test/helpers/testabilityPatch.js
index ef03ad6bff8a..74fd2f76add6 100644
--- a/test/helpers/testabilityPatch.js
+++ b/test/helpers/testabilityPatch.js
@@ -37,6 +37,17 @@ beforeEach(function() {
afterEach(function() {
var count, cache;
+ // both of these nodes are persisted across tests
+ // and therefore the hashCode may be cached
+ var node = document.querySelector('html');
+ if (node) {
+ node.$$hashKey = null;
+ }
+ var bod = document.body;
+ if (bod) {
+ bod.$$hashKey = null;
+ }
+
if (this.$injector) {
var $rootScope = this.$injector.get('$rootScope');
var $rootElement = this.$injector.get('$rootElement');
diff --git a/test/ng/animateSpec.js b/test/ng/animateSpec.js
index ed7d17d3b98a..f1ae31686f82 100644
--- a/test/ng/animateSpec.js
+++ b/test/ng/animateSpec.js
@@ -82,12 +82,14 @@ describe("$animate", function() {
expect($animate.leave(element)).toBeAPromise();
}));
- it("should provide noop `enabled` and `cancel` methods", inject(function($animate) {
- expect($animate.enabled).toBe(angular.noop);
+ it("should provide the `enabled` and `cancel` methods", inject(function($animate) {
expect($animate.enabled()).toBeUndefined();
+ expect($animate.cancel({})).toBeUndefined();
+ }));
- expect($animate.cancel).toBe(angular.noop);
- expect($animate.cancel()).toBeUndefined();
+ it("should provide the `on` and `off` methods", inject(function($animate) {
+ expect(isFunction($animate.on)).toBe(true);
+ expect(isFunction($animate.off)).toBe(true);
}));
it("should add and remove classes on SVG elements", inject(function($animate, $rootScope) {
@@ -175,6 +177,30 @@ describe("$animate", function() {
expect(style.color).toBe('green');
expect(style.borderColor).toBe('purple');
}));
+
+ it("should avoid cancelling out add/remove when the element already contains the class",
+ inject(function($animate, $rootScope) {
+
+ var element = jqLite('');
+
+ $animate.addClass(element, 'ng-hide');
+ $animate.removeClass(element, 'ng-hide');
+ $rootScope.$digest();
+
+ expect(element).not.toHaveClass('ng-hide');
+ }));
+
+ it("should avoid cancelling out remove/add if the element does not contain the class",
+ inject(function($animate, $rootScope) {
+
+ var element = jqLite('');
+
+ $animate.removeClass(element, 'ng-hide');
+ $animate.addClass(element, 'ng-hide');
+ $rootScope.$digest();
+
+ expect(element).toHaveClass('ng-hide');
+ }));
});
describe('CSS class DOM manipulation', function() {
@@ -190,26 +216,27 @@ describe("$animate", function() {
function setupClassManipulationSpies() {
inject(function($animate) {
- addClass = spyOn($animate, '$$addClassImmediately').andCallThrough();
- removeClass = spyOn($animate, '$$removeClassImmediately').andCallThrough();
+ addClass = spyOn(window, 'jqLiteAddClass').andCallThrough();
+ removeClass = spyOn(window, 'jqLiteRemoveClass').andCallThrough();
});
}
function setupClassManipulationLogger(log) {
- inject(function($animate) {
- var addClassImmediately = $animate.$$addClassImmediately;
- var removeClassImmediately = $animate.$$removeClassImmediately;
- addClass = spyOn($animate, '$$addClassImmediately').andCallFake(function(element, classes) {
+ inject(function() {
+ var _addClass = jqLiteAddClass;
+ addClass = spyOn(window, 'jqLiteAddClass').andCallFake(function(element, classes) {
var names = classes;
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join(' ');
log('addClass(' + names + ')');
- return addClassImmediately.call($animate, element, classes);
+ return _addClass(element, classes);
});
- removeClass = spyOn($animate, '$$removeClassImmediately').andCallFake(function(element, classes) {
+
+ var _removeClass = jqLiteRemoveClass;
+ removeClass = spyOn(window, 'jqLiteRemoveClass').andCallFake(function(element, classes) {
var names = classes;
if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join(' ');
log('removeClass(' + names + ')');
- return removeClassImmediately.call($animate, element, classes);
+ return _removeClass(element, classes);
});
});
}
@@ -281,7 +308,7 @@ describe("$animate", function() {
}));
- it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) {
+ it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $$rAF, $rootScope) {
element = jqLite('
test
');
$animate.addClass(element, 'test1').then(log.fn('addClass(test1)'));
@@ -289,7 +316,8 @@ describe("$animate", function() {
$rootScope.$digest();
expect(log).toEqual([]);
- $browser.defer.flush();
+ $$rAF.flush();
+ $rootScope.$digest();
expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']);
log.reset();
@@ -298,10 +326,10 @@ describe("$animate", function() {
$rootScope.$apply(function() {
$animate.addClass(element, 'test3').then(log.fn('addClass(test3)'));
$animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)'));
- expect(log).toEqual([]);
});
- $browser.defer.flush();
+ $$rAF.flush();
+ $rootScope.$digest();
expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']);
}));
diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js
index cd74f9defba1..27d18f3032e0 100644
--- a/test/ng/directive/formSpec.js
+++ b/test/ng/directive/formSpec.js
@@ -802,6 +802,7 @@ describe('form', function() {
scope.$digest();
expect(form).toBePristine();
scope.$digest();
+
expect(formCtrl.$pristine).toBe(true);
expect(formCtrl.$dirty).toBe(false);
expect(nestedForm).toBePristine();
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index 0967aa5898fa..f9f8f044ad89 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -425,14 +425,13 @@ describe('ngClass animations', function() {
});
});
- it("should consider the ngClass expression evaluation before performing an animation", function() {
+ it("should combine the ngClass evaluation with the enter animation", function() {
//mocks are not used since the enter delegation method is called before addClass and
//it makes it impossible to test to see that addClass is called first
module('ngAnimate');
module('ngAnimateMock');
- var digestQueue = [];
module(function($animateProvider) {
$animateProvider.register('.crazy', function() {
return {
@@ -442,25 +441,9 @@ describe('ngClass animations', function() {
}
};
});
-
- return function($rootScope) {
- var before = $rootScope.$$postDigest;
- $rootScope.$$postDigest = function() {
- var args = arguments;
- digestQueue.push(function() {
- before.apply($rootScope, args);
- });
- };
- };
});
- inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $document) {
-
- // Animations need to digest twice in order to be enabled regardless if there are no template HTTP requests.
- $rootScope.$digest();
- digestQueue.shift()();
-
- $rootScope.$digest();
- digestQueue.shift()();
+ inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $document, $$rAF) {
+ $animate.enabled(true);
$rootScope.val = 'crazy';
element = angular.element('');
@@ -478,25 +461,13 @@ describe('ngClass animations', function() {
expect(element.hasClass('crazy')).toBe(false);
expect(enterComplete).toBe(false);
- expect(digestQueue.length).toBe(1);
$rootScope.$digest();
-
- $timeout.flush();
-
- expect(element.hasClass('crazy')).toBe(true);
- expect(enterComplete).toBe(false);
-
- digestQueue.shift()(); //enter
- expect(digestQueue.length).toBe(0);
-
- //we don't normally need this, but since the timing between digests
- //is spaced-out then it is required so that the original digestion
- //is kicked into gear
+ $$rAF.flush();
$rootScope.$digest();
- $animate.triggerCallbacks();
- expect(element.data('state')).toBe('crazy-enter');
+ expect(element.hasClass('crazy')).toBe(true);
expect(enterComplete).toBe(true);
+ expect(element.data('state')).toBe('crazy-enter');
});
});
diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js
index 438fef1325e4..a5beb214f784 100644
--- a/test/ng/directive/ngModelSpec.js
+++ b/test/ng/directive/ngModelSpec.js
@@ -1156,17 +1156,17 @@ describe('ngModel', function() {
it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) {
- var addClass = $animate.$$addClassImmediately;
- var removeClass = $animate.$$removeClassImmediately;
+ var addClass = $animate.addClass;
+ var removeClass = $animate.removeClass;
var addClassCallCount = 0;
var removeClassCallCount = 0;
var input;
- $animate.$$addClassImmediately = function(element, className) {
+ $animate.addClass = function(element, className) {
if (input && element[0] === input[0]) ++addClassCallCount;
return addClass.call($animate, element, className);
};
- $animate.$$removeClassImmediately = function(element, className) {
+ $animate.removeClass = function(element, className) {
if (input && element[0] === input[0]) ++removeClassCallCount;
return removeClass.call($animate, element, className);
};
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
index 1205a8701b9f..c4377dcab160 100644
--- a/test/ng/directive/ngRepeatSpec.js
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -1462,7 +1462,7 @@ describe('ngRepeat animations', function() {
}));
it('should not change the position of the block that is being animated away via a leave animation',
- inject(function($compile, $rootScope, $animate, $document, $window, $sniffer, $timeout) {
+ inject(function($compile, $rootScope, $animate, $document, $window, $sniffer, $timeout, $$rAF) {
if (!$sniffer.transitions) return;
var item;
@@ -1487,10 +1487,9 @@ describe('ngRepeat animations', function() {
$rootScope.$digest();
expect(element.text()).toBe('123'); // the original order should be preserved
- $animate.triggerReflow();
+ $$rAF.flush();
$timeout.flush(1500); // 1s * 1.5 closing buffer
expect(element.text()).toBe('13');
-
} finally {
ss.destroy();
}
diff --git a/test/ngAnimate/.jshintrc b/test/ngAnimate/.jshintrc
new file mode 100644
index 000000000000..b307fb405952
--- /dev/null
+++ b/test/ngAnimate/.jshintrc
@@ -0,0 +1,13 @@
+{
+ "extends": "../.jshintrc",
+ "browser": true,
+ "newcap": false,
+ "globals": {
+ "mergeAnimationOptions": false,
+ "prepareAnimationOptions": false,
+ "applyAnimationStyles": false,
+ "applyAnimationFromStyles": false,
+ "applyAnimationToStyles": false,
+ "applyAnimationClassesFactory": false
+ }
+}
diff --git a/test/ngAnimate/animateCssDriverSpec.js b/test/ngAnimate/animateCssDriverSpec.js
new file mode 100644
index 000000000000..81a9e14cda42
--- /dev/null
+++ b/test/ngAnimate/animateCssDriverSpec.js
@@ -0,0 +1,894 @@
+'use strict';
+
+describe("ngAnimate $$animateCssDriver", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function int(x) {
+ return parseInt(x, 10);
+ }
+
+ function hasAll(array, vals) {
+ for (var i = 0; i < vals.length; i++) {
+ if (array.indexOf(vals[i]) === -1) return false;
+ }
+ return true;
+ }
+
+ it('should return a noop driver handler if the browser does not support CSS transitions and keyframes', function() {
+ module(function($provide) {
+ $provide.value('$sniffer', {});
+ });
+ inject(function($$animateCssDriver) {
+ expect($$animateCssDriver).toBe(noop);
+ });
+ });
+
+ describe('when active', function() {
+ if (!browserSupportsCssAnimations()) return;
+
+ var element;
+ var ss;
+ afterEach(function() {
+ dealoc(element);
+ if (ss) {
+ ss.destroy();
+ }
+ });
+
+ var capturedAnimation;
+ var captureLog;
+ var driver;
+ var captureFn;
+ beforeEach(module(function($provide) {
+ capturedAnimation = null;
+ captureLog = [];
+ captureFn = noop;
+
+ $provide.factory('$animateCss', function($$AnimateRunner) {
+ return function() {
+ var runner = new $$AnimateRunner();
+
+ capturedAnimation = arguments;
+ captureFn.apply(null, arguments);
+ captureLog.push({
+ element: arguments[0],
+ args: arguments,
+ runner: runner
+ });
+
+ return {
+ start: function() {
+ return runner;
+ }
+ };
+ };
+ });
+
+ element = jqLite('');
+
+ return function($$animateCssDriver, $document, $window) {
+ driver = $$animateCssDriver;
+ ss = createMockStyleSheet($document, $window);
+ };
+ }));
+
+ it('should register the $$animateCssDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateCssDriver');
+ }));
+
+ it('should register the $$animateCssDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateCssDriver');
+ }));
+
+ describe("regular animations", function() {
+ it("should render an animation on the given element", inject(function() {
+ driver({ element: element });
+ expect(capturedAnimation[0]).toBe(element);
+ }));
+
+ it("should return an object with a start function", inject(function() {
+ var runner = driver({ element: element });
+ expect(isFunction(runner.start)).toBeTruthy();
+ }));
+ });
+
+ describe("anchored animations", function() {
+ var from, to, fromAnimation, toAnimation;
+
+ beforeEach(module(function() {
+ return function($rootElement, $document) {
+ from = element;
+ to = jqLite('');
+ fromAnimation = { element: from, event: 'enter' };
+ toAnimation = { element: to, event: 'leave' };
+ $rootElement.append(from);
+ $rootElement.append(to);
+
+ // we need to do this so that style detection works
+ jqLite($document[0].body).append($rootElement);
+ };
+ }));
+
+ it("should not return anything if no animation is detected", function() {
+ module(function($provide) {
+ $provide.value('$animateCss', noop);
+ });
+ inject(function() {
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+ expect(runner).toBeFalsy();
+ });
+ });
+
+ it("should return a start method", inject(function() {
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+ expect(isFunction(animator.start)).toBeTruthy();
+ }));
+
+ they("should return a runner with a $prop() method which will end the animation",
+ ['end', 'cancel'], function(method) {
+
+ var closeAnimation;
+ module(function($provide) {
+ $provide.factory('$animateCss', function($q, $$AnimateRunner) {
+ return function() {
+ return {
+ start: function() {
+ return new $$AnimateRunner({
+ end: function() {
+ closeAnimation();
+ }
+ });
+ }
+ };
+ };
+ });
+ });
+
+ inject(function() {
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ var animationClosed = false;
+ closeAnimation = function() {
+ animationClosed = true;
+ };
+
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+ runner[method]();
+ expect(animationClosed).toBe(true);
+ });
+ });
+
+ it("should end the animation for each of the from and to elements as well as all the anchors", function() {
+ var closeLog = {};
+ module(function($provide) {
+ $provide.factory('$animateCss', function($q, $$AnimateRunner) {
+ return function(element, options) {
+ var type = options.event || 'anchor';
+ closeLog[type] = closeLog[type] || [];
+ return {
+ start: function() {
+ return new $$AnimateRunner({
+ end: function() {
+ closeLog[type].push(element);
+ }
+ });
+ }
+ };
+ };
+ });
+ });
+
+ inject(function() {
+ //we'll just use one animation to make the test smaller
+ var anchorAnimation = {
+ 'in': jqLite(''),
+ 'out': jqLite('')
+ };
+
+ fromAnimation.element.append(anchorAnimation['out']);
+ toAnimation.element.append(anchorAnimation['in']);
+
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [
+ anchorAnimation,
+ anchorAnimation,
+ anchorAnimation
+ ]
+ });
+
+ var runner = animator.start();
+ runner.end();
+
+ expect(closeLog.enter[0]).toEqual(fromAnimation.element);
+ expect(closeLog.leave[0]).toEqual(toAnimation.element);
+ expect(closeLog.anchor.length).toBe(3);
+ });
+ });
+
+ it("should render an animation on both the from and to elements", inject(function() {
+ captureFn = function(element, details) {
+ element.addClass(details.event);
+ };
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ expect(captureLog.length).toBe(2);
+ expect(fromAnimation.element).toHaveClass('enter');
+ expect(toAnimation.element).toHaveClass('leave');
+ }));
+
+ it("should start the animations on the from and to elements in parallel", function() {
+ var animationLog = [];
+ module(function($provide) {
+ $provide.factory('$animateCss', function($$AnimateRunner) {
+ return function(element, details) {
+ return {
+ start: function() {
+ animationLog.push([element, details.event]);
+ return new $$AnimateRunner();
+ }
+ };
+ };
+ });
+ });
+ inject(function() {
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ expect(animationLog.length).toBe(0);
+ runner.start();
+ expect(animationLog).toEqual([
+ [fromAnimation.element, 'enter'],
+ [toAnimation.element, 'leave']
+ ]);
+ });
+ });
+
+ it("should start an animation for each anchor", inject(function() {
+ var o1 = jqLite('');
+ from.append(o1);
+ var o2 = jqLite('');
+ from.append(o2);
+ var o3 = jqLite('');
+ from.append(o3);
+
+ var i1 = jqLite('');
+ to.append(i1);
+ var i2 = jqLite('');
+ to.append(i2);
+ var i3 = jqLite('');
+ to.append(i3);
+
+ var anchors = [
+ { 'out': o1, 'in': i1, classes: 'red' },
+ { 'out': o2, 'in': i2, classes: 'blue' },
+ { 'out': o2, 'in': i2, classes: 'green' }
+ ];
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: anchors
+ });
+
+ expect(captureLog.length).toBe(5);
+ }));
+
+ it("should create a clone of the starting element for each anchor animation", inject(function() {
+ var o1 = jqLite('');
+ from.append(o1);
+ var o2 = jqLite('');
+ from.append(o2);
+
+ var i1 = jqLite('');
+ to.append(i1);
+ var i2 = jqLite('');
+ to.append(i2);
+
+ var anchors = [
+ { 'out': o1, 'in': i1 },
+ { 'out': o2, 'in': i2 }
+ ];
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: anchors
+ });
+
+ var a2 = captureLog.pop().element;
+ var a1 = captureLog.pop().element;
+
+ expect(a1).not.toEqual(o1);
+ expect(a1.attr('class')).toMatch(/\bout1\b/);
+ expect(a2).not.toEqual(o2);
+ expect(a2.attr('class')).toMatch(/\bout2\b/);
+ }));
+
+ it("should create a clone of the starting element and place it at the end of the $rootElement container",
+ inject(function($rootElement) {
+
+ //stick some garbage into the rootElement
+ $rootElement.append(jqLite(''));
+ $rootElement.append(jqLite(''));
+ $rootElement.append(jqLite(''));
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'in': fromAnchor,
+ 'out': toAnchor
+ }]
+ });
+
+ var anchor = captureLog.pop().element;
+ var anchorNode = anchor[0];
+ var contents = $rootElement.contents();
+
+ expect(contents.length).toBeGreaterThan(1);
+ expect(contents[contents.length - 1]).toEqual(anchorNode);
+ }));
+
+ it("should first do an addClass('ng-anchor-out') animation on the cloned anchor", inject(function($rootElement) {
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.addClass).toBe('ng-anchor-out');
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ it("should then do an addClass('ng-anchor-in') animation on the cloned anchor and remove the old class",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.removeClass.trim()).toBe('ng-anchor-out');
+ expect(anchorDetails.addClass.trim()).toBe('ng-anchor-in');
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ it("should provide an explicit delay setting in the options provided to $animateCss for anchor animations",
+ inject(function($rootElement) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ expect(capturedAnimation[1].delay).toBeTruthy();
+ }));
+
+ it("should begin the anchor animation by seeding the from styles based on where the from anchor element is positioned",
+ inject(function($rootElement) {
+
+ ss.addRule('.starting-element', 'width:200px; height:100px; display:block;');
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorAnimation = captureLog.pop();
+ var anchorElement = anchorAnimation.element;
+ var anchorDetails = anchorAnimation.args[1];
+
+ var fromStyles = anchorDetails.from;
+ expect(int(fromStyles.width)).toBe(200);
+ expect(int(fromStyles.height)).toBe(100);
+ // some browsers have their own body margin defaults
+ expect(int(fromStyles.top)).toBeGreaterThan(499);
+ expect(int(fromStyles.left)).toBeGreaterThan(149);
+ }));
+
+ it("should append a `px` value for all seeded animation styles", inject(function($rootElement, $$rAF) {
+ ss.addRule('.starting-element', 'width:10px; height:20px; display:inline-block;');
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorAnimation = captureLog.pop();
+ var anchorDetails = anchorAnimation.args[1];
+
+ forEach(anchorDetails.from, function(value) {
+ expect(value.substr(value.length - 2)).toBe('px');
+ });
+
+ // the out animation goes first
+ anchorAnimation.runner.end();
+ $$rAF.flush();
+
+ anchorAnimation = captureLog.pop();
+ anchorDetails = anchorAnimation.args[1];
+
+ forEach(anchorDetails.to, function(value) {
+ expect(value.substr(value.length - 2)).toBe('px');
+ });
+ }));
+
+ it("should then do an removeClass('out') + addClass('in') animation on the cloned anchor",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.removeClass).toMatch(/\bout\b/);
+ expect(anchorDetails.addClass).toMatch(/\bin\b/);
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ it("should add the `ng-animate-anchor` class to the cloned anchor element",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ var clonedAnchor = captureLog.pop().element;
+ expect(clonedAnchor).toHaveClass('ng-animate-anchor');
+ }));
+
+ it("should add and remove the `ng-animate-shim` class on the in anchor element during the animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ expect(fromAnchor).toHaveClass('ng-animate-shim');
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+ captureLog.pop().runner.end();
+
+ expect(fromAnchor).not.toHaveClass('ng-animate-shim');
+ }));
+
+ it("should add and remove the `ng-animate-shim` class on the out anchor element during the animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ expect(toAnchor).toHaveClass('ng-animate-shim');
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ expect(toAnchor).toHaveClass('ng-animate-shim');
+ captureLog.pop().runner.end();
+
+ expect(toAnchor).not.toHaveClass('ng-animate-shim');
+ }));
+
+ it("should create the cloned anchor with all of the classes from the from anchor element",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ var addedClasses = captureLog.pop().element.attr('class').split(' ');
+ expect(hasAll(addedClasses, ['yes', 'no', 'maybe'])).toBe(true);
+ }));
+
+ it("should remove the classes of the starting anchor from the cloned anchor node during the in animation and also add the classes of the destination anchor within the same animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ var removedClasses = anchorDetails.removeClass.split(' ');
+ var addedClasses = anchorDetails.addClass.split(' ');
+
+ expect(hasAll(removedClasses, ['yes', 'no', 'maybe'])).toBe(true);
+ expect(hasAll(addedClasses, ['why', 'ok', 'so-what'])).toBe(true);
+ }));
+
+ it("should not attempt to add/remove any classes that contain a `ng-` prefix",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var inAnimation = captureLog.pop();
+ var details = inAnimation.args[1];
+
+ var addedClasses = details.addClass.split(' ');
+ var removedClasses = details.removeClass.split(' ');
+
+ expect(addedClasses).not.toContain('ng-foo');
+ expect(addedClasses).not.toContain('ng-bar');
+
+ expect(removedClasses).not.toContain('ng-yes');
+ expect(removedClasses).not.toContain('ng-no');
+ }));
+
+ it("should not remove any shared CSS classes between the starting and destination anchor element during the in animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var outAnimation = captureLog.pop();
+ var clonedAnchor = outAnimation.element;
+ var details = outAnimation.args[1];
+
+ var addedClasses = details.addClass.split(' ');
+ var removedClasses = details.removeClass.split(' ');
+
+ expect(hasAll(addedClasses, ['brown', 'black'])).toBe(true);
+ expect(hasAll(removedClasses, ['green'])).toBe(true);
+
+ expect(addedClasses).not.toContain('red');
+ expect(addedClasses).not.toContain('blue');
+
+ expect(removedClasses).not.toContain('brown');
+ expect(removedClasses).not.toContain('black');
+
+ expect(clonedAnchor).toHaveClass('red');
+ expect(clonedAnchor).toHaveClass('blue');
+ }));
+
+ it("should continue the anchor animation by seeding the to styles based on where the final anchor element will be positioned",
+ inject(function($rootElement, $$rAF) {
+ ss.addRule('.ending-element', 'width:9999px; height:6666px; display:inline-block;');
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorAnimation = captureLog.pop();
+ var anchorElement = anchorAnimation.element;
+ var anchorDetails = anchorAnimation.args[1];
+
+ var toStyles = anchorDetails.to;
+ expect(int(toStyles.width)).toBe(9999);
+ expect(int(toStyles.height)).toBe(6666);
+ // some browsers have their own body margin defaults
+ expect(int(toStyles.top)).toBeGreaterThan(300);
+ expect(int(toStyles.left)).toBeGreaterThan(20);
+ }));
+
+ it("should remove the cloned anchor node from the DOM once the 'in' animation is complete",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ var inAnimation = captureLog.pop();
+ var clonedAnchor = inAnimation.element;
+ expect(clonedAnchor.parent().length).toBe(1);
+ inAnimation.runner.end();
+ $$rAF.flush();
+
+ // now the in animation completes
+ expect(clonedAnchor.parent().length).toBe(1);
+ captureLog.pop().runner.end();
+
+ expect(clonedAnchor.parent().length).toBe(0);
+ }));
+
+ it("should pass the provided domOperation into $animateCss to be run right after the element is animated if a leave animation is present",
+ inject(function($rootElement, $$rAF) {
+
+ toAnimation.event = 'enter';
+ fromAnimation.event = 'leave';
+
+ var leaveOp = function() { };
+ fromAnimation.domOperation = leaveOp;
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation
+ }).start();
+
+ var leaveAnimation = captureLog.shift();
+ var enterAnimation = captureLog.shift();
+
+ expect(leaveAnimation.args[1].onDone).toBe(leaveOp);
+ expect(enterAnimation.args[1].onDone).toBeUndefined();
+ }));
+
+ it("should fire the returned runner promise when the from, to and anchor animations are all complete",
+ inject(function($rootElement, $rootScope, $$rAF) {
+
+ ss.addRule('.ending-element', 'width:9999px; height:6666px; display:inline-block;');
+
+ var fromAnchor = jqLite('');
+ from.append(fromAnchor);
+
+ var toAnchor = jqLite('');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var completed = false;
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start().then(function() {
+ completed = true;
+ });
+
+ captureLog.pop().runner.end(); //from
+ captureLog.pop().runner.end(); //to
+ captureLog.pop().runner.end(); //anchor(out)
+ captureLog.pop().runner.end(); //anchor(in)
+ $$rAF.flush();
+ $rootScope.$digest();
+
+ expect(completed).toBe(true);
+ }));
+ });
+ });
+});
diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js
new file mode 100644
index 000000000000..afcc52efbf40
--- /dev/null
+++ b/test/ngAnimate/animateCssSpec.js
@@ -0,0 +1,2428 @@
+'use strict';
+
+describe("ngAnimate $animateCss", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function assertAnimationRunning(element, not) {
+ var className = element.attr('class');
+ var regex = /\b\w+-active\b/;
+ not ? expect(className).toMatch(regex)
+ : expect(className).not.toMatch(regex);
+ }
+
+ var fakeStyle = {
+ color: 'blue'
+ };
+
+ var ss, prefix, triggerAnimationStartFrame;
+ beforeEach(module(function() {
+ return function($document, $window, $sniffer, $$rAF) {
+ prefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-';
+ ss = createMockStyleSheet($document, $window);
+ triggerAnimationStartFrame = function() {
+ $$rAF.flush();
+ };
+ };
+ }));
+
+ afterEach(function() {
+ if (ss) {
+ ss.destroy();
+ }
+ });
+
+ it("should return false if neither transitions or keyframes are supported by the browser",
+ inject(function($animateCss, $sniffer, $rootElement, $document) {
+
+ var animator;
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $sniffer.transitions = $sniffer.animations = false;
+ animator = $animateCss(element, {
+ duration: 10,
+ to: { 'background': 'red' }
+ });
+ expect(animator).toBeFalsy();
+ }));
+
+ describe('when active', function() {
+ if (!browserSupportsCssAnimations()) return;
+
+ describe("rAF usage", function() {
+ it("should buffer all requests into a single requestAnimationFrame call",
+ inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ var count = 0;
+ var runners = [];
+ function makeRequest() {
+ var element = jqLite('');
+ $rootElement.append(element);
+ var runner = $animateCss(element, { duration: 5, to: fakeStyle }).start();
+ runner.then(function() {
+ count++;
+ });
+ runners.push(runner);
+ }
+
+ makeRequest();
+ makeRequest();
+ makeRequest();
+
+ expect(count).toBe(0);
+
+ triggerAnimationStartFrame();
+ forEach(runners, function(runner) {
+ runner.end();
+ });
+
+ $rootScope.$digest();
+ expect(count).toBe(3);
+ }));
+
+ it("should cancel previous requests to rAF to avoid premature flushing", function() {
+ var count = 0;
+ module(function($provide) {
+ $provide.value('$$rAF', function() {
+ return function cancellationFn() {
+ count++;
+ };
+ });
+ });
+ inject(function($animateCss, $$rAF, $document, $rootElement) {
+ jqLite($document[0].body).append($rootElement);
+
+ function makeRequest() {
+ var element = jqLite('');
+ $rootElement.append(element);
+ $animateCss(element, { duration: 5, to: fakeStyle }).start();
+ }
+
+ makeRequest();
+ makeRequest();
+ makeRequest();
+ expect(count).toBe(2);
+ });
+ });
+ });
+
+ describe("animator and runner", function() {
+ var animationDuration = 5;
+ var element, animator;
+ beforeEach(inject(function($animateCss, $rootElement, $document) {
+ element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true,
+ duration: animationDuration,
+ to: fakeStyle
+ });
+ }));
+
+ it('should expose start and end functions for the animator object', inject(function() {
+ expect(typeof animator.start).toBe('function');
+ expect(typeof animator.end).toBe('function');
+ }));
+
+ it('should expose end, cancel, resume and pause methods on the runner object', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(typeof runner.end).toBe('function');
+ expect(typeof runner.cancel).toBe('function');
+ expect(typeof runner.resume).toBe('function');
+ expect(typeof runner.pause).toBe('function');
+ }));
+
+ it('should start the animation', inject(function() {
+ expect(element).not.toHaveClass('ng-enter-active');
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-enter-active');
+ }));
+
+ it('should end the animation when called from the animator object', inject(function() {
+ animator.start();
+ triggerAnimationStartFrame();
+
+ animator.end();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should end the animation when called from the runner object', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+ runner.end();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should permanently close the animation if closed before the next rAF runs', inject(function() {
+ var runner = animator.start();
+ runner.end();
+
+ triggerAnimationStartFrame();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should return a runner object at the start of the animation that contains a `then` method',
+ inject(function($rootScope) {
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(isPromiseLike(runner)).toBeTruthy();
+
+ var resolved;
+ runner.then(function() {
+ resolved = true;
+ });
+
+ runner.end();
+ $rootScope.$digest();
+ expect(resolved).toBeTruthy();
+ }));
+
+ it('should cancel the animation and reject', inject(function($rootScope) {
+ var rejected;
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.then(noop, function() {
+ rejected = true;
+ });
+
+ runner.cancel();
+ $rootScope.$digest();
+ expect(rejected).toBeTruthy();
+ }));
+
+ it('should run pause, but not effect the transition animation', inject(function() {
+ var blockingDelay = '-' + animationDuration + 's';
+
+ expect(element.css('transition-delay')).toEqual(blockingDelay);
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css('transition-delay')).not.toEqual(blockingDelay);
+ runner.pause();
+ expect(element.css('transition-delay')).not.toEqual(blockingDelay);
+ }));
+
+ it('should pause the transition, have no effect, but not end it', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 5 });
+
+ expect(element).toHaveClass('ng-enter-active');
+ }));
+
+ it('should resume the animation', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 5 });
+
+ expect(element).toHaveClass('ng-enter-active');
+ runner.resume();
+
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should pause and resume a keyframe animation using animation-play-state',
+ inject(function($animateCss) {
+
+ element.attr('style', '');
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+ expect(element.css(prefix + 'animation-play-state')).toEqual('paused');
+ runner.resume();
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it('should remove the animation-play-state style if the animation is closed',
+ inject(function($animateCss) {
+
+ element.attr('style', '');
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+ expect(element.css(prefix + 'animation-play-state')).toEqual('paused');
+ runner.end();
+ expect(element.attr('style')).toBeFalsy();
+ }));
+ });
+
+ describe("CSS", function() {
+ describe("detected styles", function() {
+ var element, options;
+
+ function assertAnimationComplete(bool) {
+ var assert = expect(element);
+ if (bool) {
+ assert = assert.not;
+ }
+ assert.toHaveClass('ng-enter');
+ assert.toHaveClass('ng-enter-active');
+ }
+
+ function keyframeProgress(element, duration, delay) {
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration });
+ }
+
+ function transitionProgress(element, duration, delay) {
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration });
+ }
+
+ beforeEach(inject(function($rootElement, $document) {
+ element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+ options = { event: 'enter', structural: true };
+ }));
+
+ it("should use the highest transition duration value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-duration:10s, 15s, 20s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 20);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest transition delay value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-delay:10s, 15s, 20s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 1, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 20);
+ assertAnimationComplete(true);
+ }));
+
+ it("should only close when both the animation delay and duration have passed",
+ inject(function($animateCss) {
+
+ ss.addRule('.ng-enter', 'transition:10s 5s linear all;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ transitionProgress(element, 10, 2);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 9, 6);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 10, 5);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe duration value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'animation:animation 1s, animation 2s, animation 3s;' +
+ '-webkit-animation:animation 1s, animation 2s, animation 3s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 3);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe delay value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'animation:animation 1s 2s, animation 1s 10s, animation 1s 1000ms;' +
+ '-webkit-animation:animation 1s 2s, animation 1s 10s, animation 1s 1000ms;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 10);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe duration value detected in the CSS class with respect to the animation-iteration-count property", inject(function($animateCss) {
+ ss.addRule('.ng-enter',
+ 'animation:animation 1s 2s 3, animation 1s 10s 2, animation 1s 1000ms infinite;' +
+ '-webkit-animation:animation 1s 2s 3, animation 1s 10s 2, animation 1s 1000ms infinite;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 3, 10);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest duration value when both transitions and keyframes are used", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-duration:10s, 15s, 20s;' +
+ 'animation:animation 1s, animation 2s, animation 3s 0s 7;' +
+ '-webkit-animation:animation 1s, animation 2s, animation 3s 0s 7;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 10);
+ keyframeProgress(element, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 15);
+ keyframeProgress(element, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 20);
+ keyframeProgress(element, 20);
+ assertAnimationComplete(false);
+
+ // 7 * 3 = 21
+ transitionProgress(element, 21);
+ keyframeProgress(element, 21);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest delay value when both transitions and keyframes are used", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-delay:10s, 15s, 20s;' +
+ 'animation:animation 1s 2s, animation 1s 16s, animation 1s 19s;' +
+ '-webkit-animation:animation 1s 2s, animation 1s 16s, animation 1s 19s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 1, 10);
+ keyframeProgress(element, 1, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 16);
+ keyframeProgress(element, 1, 16);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 19);
+ keyframeProgress(element, 1, 19);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 20);
+ keyframeProgress(element, 1, 20);
+ assertAnimationComplete(true);
+ }));
+ });
+
+ describe("staggering", function() {
+ it("should apply a stagger based when an active ng-EVENT-stagger class with a transition-delay is detected",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.ng-enter', 'transition:2s linear all');
+
+ var elements = [];
+ var i;
+ var elm;
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+ $timeout.flush(200);
+ expect(elm).toHaveClass('ng-enter-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('ng-enter');
+ expect(elm).not.toHaveClass('ng-enter-active');
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ }
+ }));
+
+ it("should apply a stagger based when for all provided addClass/removeClass CSS classes",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.red-add-stagger,' +
+ '.blue-remove-stagger,' +
+ '.green-add-stagger', 'transition-delay:0.2s');
+
+ ss.addRule('.red-add,' +
+ '.blue-remove,' +
+ '.green-add', 'transition:2s linear all');
+
+ var elements = [];
+ var i;
+ var elm;
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, {
+ addClass: 'red green',
+ removeClass: 'blue'
+ }).start();
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 0; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('red-add-stagger');
+ expect(elm).not.toHaveClass('green-add-stagger');
+ expect(elm).not.toHaveClass('blue-remove-stagger');
+
+ expect(elm).toHaveClass('red-add');
+ expect(elm).toHaveClass('green-add');
+ expect(elm).toHaveClass('blue-remove');
+ }
+
+ expect(elements[0]).toHaveClass('red-add-active');
+ expect(elements[0]).toHaveClass('green-add-active');
+ expect(elements[0]).toHaveClass('blue-remove-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('red-add-active');
+ expect(elm).not.toHaveClass('green-add-active');
+ expect(elm).not.toHaveClass('blue-remove-active');
+
+ $timeout.flush(200);
+
+ expect(elm).toHaveClass('red-add-active');
+ expect(elm).toHaveClass('green-add-active');
+ expect(elm).toHaveClass('blue-remove-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('red-add-active');
+ expect(elm).not.toHaveClass('green-add-active');
+ expect(elm).not.toHaveClass('blue-remove-active');
+
+ expect(elm).not.toHaveClass('red-add-stagger');
+ expect(elm).not.toHaveClass('green-add-stagger');
+ expect(elm).not.toHaveClass('blue-remove-stagger');
+ }
+ }));
+
+ it("should block the transition animation between start and animate when staggered",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.ng-enter', 'transition:2s linear all;');
+
+ var element;
+ var i;
+ var elms = [];
+
+ for (i = 0; i < 5; i++) {
+ element = jqLite('');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ elms.push(element);
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 0; i < 5; i++) {
+ element = elms[i];
+ if (i === 0) {
+ expect(element.attr('style')).toBeFalsy();
+ } else {
+ expect(element.css('transition-delay')).toContain('-2s');
+ }
+ }
+ }));
+
+ it("should block (pause) the keyframe animation between start and animate when staggered",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
+ ss.addRule('.ng-enter', prefix + 'animation:my_animation 2s;');
+
+ var i, element, elements = [];
+ for (i = 0; i < 5; i++) {
+ element = jqLite('');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ elements.push(element);
+ }
+
+ triggerAnimationStartFrame();
+
+ for (i = 0; i < 5; i++) {
+ element = elements[i];
+ if (i === 0) { // the first element is always run right away
+ expect(element.attr('style')).toBeFalsy();
+ } else {
+ expect(element.css(prefix + 'animation-play-state')).toBe('paused');
+ }
+ }
+ }));
+
+ it("should not apply a stagger if the transition delay value is inherited from a earlier CSS class",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+
+ for (var i = 0; i < 5; i++) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should apply a stagger only if the transition duration value is zero when inherited from a earlier CSS class",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+ ss.addRule('.transition-animation.ng-enter-stagger',
+ 'transition-duration:0s; transition-delay:0.2s;');
+
+ var element, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ element = jqLite('');
+ $rootElement.append(element);
+
+ elms.push(element);
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 1; i < 5; i++) {
+ element = elms[i];
+ expect(element).not.toHaveClass('ng-enter-active');
+ }
+ }));
+
+
+ it("should ignore animation staggers if only transition animations were detected",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+
+ for (var i = 0; i < 5; i++) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should ignore transition staggers if only keyframe animations were detected",
+ inject(function($animateCss, $document, $rootElement) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.transition-animation', prefix + 'animation:2s 5s my_animation;');
+
+ for (var i = 0; i < 5; i++) {
+ var elm = jqLite('');
+ $rootElement.append(elm);
+
+ var animator = $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(elm).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should start on the highest stagger value if both transition and keyframe staggers are used together",
+ inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.5s;' +
+ prefix + 'animation-delay:1s');
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;' +
+ prefix + 'animation:my_animation 20s');
+
+ var i, elm, elements = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+
+ $timeout.flush(500);
+ expect(elm).not.toHaveClass('ng-enter-active');
+
+ $timeout.flush(500);
+ expect(elm).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should apply the closing timeout ontop of the stagger timeout",
+ inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var elm, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elms.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+ }
+
+ for (i = 1; i < 2; i++) {
+ elm = elms[i];
+ expect(elm).toHaveClass('ng-enter');
+ $timeout.flush(1000);
+ $timeout.flush(15000);
+ expect(elm).not.toHaveClass('ng-enter');
+ }
+ }));
+
+ it("should apply the closing timeout ontop of the stagger timeout with an added delay",
+ inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
+ ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:50s;');
+
+ var elm, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elms.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+ }
+
+ for (i = 1; i < 2; i++) {
+ elm = elms[i];
+ expect(elm).toHaveClass('ng-enter');
+ $timeout.flush(1000);
+ $timeout.flush(65000);
+ expect(elm).not.toHaveClass('ng-enter');
+ }
+ }));
+
+ it("should issue a stagger if a stagger value is provided in the options",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ jqLite($document[0].body).append($rootElement);
+ ss.addRule('.ng-enter', 'transition:2s linear all');
+
+ var elm, i, elements = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, {
+ event: 'enter',
+ structural: true,
+ stagger: 0.5
+ }).start();
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+ $timeout.flush(500);
+ expect(elm).toHaveClass('ng-enter-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('ng-enter');
+ expect(elm).not.toHaveClass('ng-enter-active');
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ }
+ }));
+
+ it("should only add/remove classes once the stagger timeout has passed",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ jqLite($document[0].body).append($rootElement);
+
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ $animateCss(element, {
+ addClass: 'red',
+ removeClass: 'green',
+ duration: 5,
+ stagger: 0.5,
+ staggerIndex: 3
+ }).start();
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('green');
+ expect(element).not.toHaveClass('red');
+
+ $timeout.flush(1500);
+ expect(element).not.toHaveClass('green');
+ expect(element).toHaveClass('red');
+ }));
+ });
+
+ describe("closing timeout", function() {
+ it("should close off the animation after 150% of the animation time has passed",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ $timeout.flush(15000);
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it("should close off the animation after 150% of the animation time has passed and consider the detected delay value",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:30s;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ $timeout.flush(45000);
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it("should still resolve the animation once expired",
+ inject(function($animateCss, $document, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+
+ var failed, passed;
+ animator.start().then(function() {
+ passed = true;
+ }, function() {
+ failed = true;
+ });
+
+ triggerAnimationStartFrame();
+ $timeout.flush(15000);
+ expect(passed).toBe(true);
+ }));
+
+ it("should not resolve/reject after passing if the animation completed successfully",
+ inject(function($animateCss, $document, $rootElement, $timeout, $rootScope) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+
+ var failed, passed;
+ animator.start().then(
+ function() {
+ passed = true;
+ },
+ function() {
+ failed = true;
+ }
+ );
+ triggerAnimationStartFrame();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ $rootScope.$digest();
+
+ expect(passed).toBe(true);
+ expect(failed).not.toBe(true);
+
+ $timeout.flush(15000);
+
+ expect(passed).toBe(true);
+ expect(failed).not.toBe(true);
+ }));
+ });
+
+ describe("getComputedStyle", function() {
+ var count;
+ var acceptableTimingsData = {
+ transitionDuration: "10s"
+ };
+
+ beforeEach(module(function($provide) {
+ count = {};
+ $provide.value('$window', extend({}, window, {
+ document: jqLite(window.document),
+ getComputedStyle: function(node) {
+ var key = node.className.indexOf('stagger') >= 0
+ ? 'stagger' : 'normal';
+ count[key] = count[key] || 0;
+ count[key]++;
+ return acceptableTimingsData;
+ }
+ }));
+
+ return function($document, $rootElement) {
+ jqLite($document[0].body).append($rootElement);
+ };
+ }));
+
+ it("should cache frequent calls to getComputedStyle before the next animation frame kicks in",
+ inject(function($animateCss, $document, $rootElement, $$rAF) {
+
+ var i, elm, animator;
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ var runner = animator.start();
+ }
+
+ expect(count.normal).toBe(1);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.normal).toBe(1);
+ triggerAnimationStartFrame();
+
+ expect(count.normal).toBe(2);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.normal).toBe(3);
+ }));
+
+ it("should cache frequent calls to getComputedStyle for stagger animations before the next animation frame kicks in",
+ inject(function($animateCss, $document, $rootElement, $$rAF) {
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(count.stagger).toBeUndefined();
+
+ var i, elm;
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.stagger).toBe(1);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.stagger).toBe(1);
+ $$rAF.flush();
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ triggerAnimationStartFrame();
+ expect(count.stagger).toBe(2);
+ }));
+ });
+ });
+
+ it('should apply a custom temporary class when a non-structural animation is used',
+ inject(function($animateCss, $rootElement, $document) {
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $animateCss(element, {
+ event: 'super',
+ duration: 1000,
+ to: fakeStyle
+ }).start();
+ expect(element).toHaveClass('super');
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('super-active');
+ }));
+
+ describe("structural animations", function() {
+ they('should decorate the element with the ng-$prop CSS class',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1000,
+ to: fakeStyle
+ });
+ expect(element).toHaveClass('ng-' + event);
+ });
+ });
+
+ they('should decorate the element with the ng-$prop-active CSS class',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1000,
+ to: fakeStyle
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should remove the ng-$prop and ng-$prop-active CSS classes from the element once the animation is done',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1,
+ to: fakeStyle
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should allow additional CSS classes to be added and removed alongside the $prop animation',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1,
+ to: fakeStyle,
+ addClass: 'red',
+ removeClass: 'green'
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('ng-' + event + '-active');
+
+ expect(element).toHaveClass('red');
+ expect(element).toHaveClass('red-add');
+ expect(element).toHaveClass('red-add-active');
+
+ expect(element).not.toHaveClass('green');
+ expect(element).toHaveClass('green-remove');
+ expect(element).toHaveClass('green-remove-active');
+ });
+ });
+
+ they('should place a CSS transition block after the preparation function to block accidental style changes',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ var animator = $animateCss(element, data);
+ expect(element.css('transition-delay')).toMatch('-1.5s');
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+ });
+ });
+
+ they('should not place a CSS transition block if options.skipBlocking is provided',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ data.skipBlocking = true;
+ var animator = $animateCss(element, data);
+
+ expect(element.attr('style')).toBeFalsy();
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+ });
+ });
+
+ they('should place a CSS transition block after the preparation function even if a duration is provided',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ data.duration = 10;
+ var animator = $animateCss(element, data);
+
+ expect(element.css('transition-delay')).toMatch('-10s');
+ expect(element.css('transition-duration')).toMatch('');
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.attr('style')).not.toContain('transition-delay');
+ expect(element.css('transition-property')).toContain('all');
+ expect(element.css('transition-duration')).toContain('10s');
+ });
+ });
+
+ it('should allow multiple events to be animated at the same time',
+ inject(function($animateCss, $rootElement, $document) {
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $animateCss(element, {
+ event: ['enter', 'leave', 'move'],
+ structural: true,
+ duration: 1,
+ to: fakeStyle
+ }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-leave');
+ expect(element).toHaveClass('ng-move');
+
+ expect(element).toHaveClass('ng-enter-active');
+ expect(element).toHaveClass('ng-leave-active');
+ expect(element).toHaveClass('ng-move-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-leave');
+ expect(element).not.toHaveClass('ng-move');
+ expect(element).not.toHaveClass('ng-enter-active');
+ expect(element).not.toHaveClass('ng-leave-active');
+ expect(element).not.toHaveClass('ng-move-active');
+ }));
+ });
+
+ describe("class-based animations", function() {
+ they('should decorate the element with the class-$prop CSS class',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {};
+ options[event + 'Class'] = 'class';
+ options.duration = 1000;
+ options.to = fakeStyle;
+ $animateCss(element, options);
+ expect(element).toHaveClass('class-' + event);
+ });
+ });
+
+ they('should decorate the element with the class-$prop-active CSS class',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {};
+ options[event + 'Class'] = 'class';
+ options.duration = 1000;
+ options.to = fakeStyle;
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('class-' + event + '-active');
+ });
+ });
+
+ they('should remove the class-$prop-add and class-$prop-active CSS classes from the element once the animation is done',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var options = {};
+ options.event = event;
+ options.duration = 10;
+ options.to = fakeStyle;
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should allow the class duration styles to be recalculated once started if the CSS classes being applied result new transition styles',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+
+ var element = jqLite('');
+
+ if (event == 'add') {
+ ss.addRule('.natural-class', 'transition:1s linear all;');
+ } else {
+ ss.addRule('.natural-class', 'transition:0s linear none;');
+ ss.addRule('.base-class', 'transition:1s linear none;');
+
+ element.addClass('base-class');
+ element.addClass('natural-class');
+ }
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var options = {};
+ options[event + 'Class'] = 'natural-class';
+ var runner = $animateCss(element, options);
+ runner.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('natural-class-' + event);
+ expect(element).toHaveClass('natural-class-' + event + '-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('natural-class-' + event);
+ expect(element).not.toHaveClass('natural-class-' + event + '-active');
+ });
+ });
+
+ they('should force the class-based values to be applied early if no transition/keyframe is detected at all',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $document) {
+
+ ss.addRule('.blue.ng-' + event, 'transition:2s linear all;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ var runner = $animateCss(element, {
+ addClass: 'blue',
+ removeClass: 'red',
+ event: event,
+ structural: true
+ });
+
+ runner.start();
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('ng-' + event + '-active');
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+ });
+ });
+ });
+
+ describe("options", function() {
+ var element;
+ beforeEach(inject(function($rootElement, $document) {
+ jqLite($document[0].body).append($rootElement);
+
+ element = jqLite('');
+ $rootElement.append(element);
+ }));
+
+ describe("[duration]", function() {
+ it("should be applied for a transition directly", inject(function($animateCss, $rootElement) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {
+ duration: 3000,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('3000s');
+ expect(style).toContain('linear');
+ }));
+
+ it("should be applied to a CSS keyframe animation directly if keyframes are detected within the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ duration: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-duration')).toEqual('5s');
+ }));
+
+ it("should remove all inline keyframe styling when an animation completes if a custom duration was applied",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ duration: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 5 });
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should remove all inline keyframe delay styling when an animation completes if a custom duration was applied",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ delay: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('5s');
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 1.5 });
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should not prepare the animation at all if a duration of zero is provided",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;');
+
+ var options = {
+ duration: 0,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ expect(animator).toBeFalsy();
+ }));
+
+ it("should apply a transition and keyframe duration directly if both transitions and keyframe classes are detected",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3s keyframe_animation;' +
+ 'animation:3s keyframe_animation;' +
+ 'transition:5s linear all;');
+
+ var options = {
+ duration: 4,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toMatch(/animation(?:-duration)?:\s*4s/);
+ expect(element.css('transition-duration')).toMatch('4s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+ });
+
+ describe("[delay]", function() {
+ it("should be applied for a transition directly", inject(function($animateCss, $rootElement) {
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {
+ duration: 3000,
+ delay: 500,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('500s');
+ }));
+
+ it("should return false for the animator if a delay is provided but not a duration",
+ inject(function($animateCss, $rootElement) {
+
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 500,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator).toBeFalsy();
+ }));
+
+ it("should override the delay value present in the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;' +
+ '-webkit-transition-delay:10s;' +
+ 'transition-delay:10s;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 500,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('500s');
+ }));
+
+ it("should allow the delay value to zero if provided",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;' +
+ '-webkit-transition-delay:10s;' +
+ 'transition-delay:10s;');
+
+ var element = jqLite('');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 0,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('0s');
+ }));
+
+ it("should be applied to a CSS keyframe animation if detected within the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ delay: 400,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('400s');
+ expect(element.attr('style')).not.toContain('transition-delay');
+ }));
+
+ it("should apply a transition and keyframe delay if both transitions and keyframe classes are detected",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3s keyframe_animation;' +
+ 'animation:3s keyframe_animation;' +
+ 'transition:5s linear all;');
+
+ var options = {
+ delay: 10,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+
+ expect(element.css('transition-delay')).toContain('-5s');
+ expect(element.attr('style')).not.toContain('animation-delay');
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('10s');
+ expect(element.css('transition-delay')).toEqual('10s');
+ }));
+
+ it("should apply blocking before the animation starts, but then apply the detected delay when options.delay is true",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition:2s linear all; transition-delay: 1s;');
+
+ var options = {
+ delay: true,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ expect(element.css('transition-delay')).toEqual('-2s');
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css('transition-delay')).toEqual('1s');
+ }));
+
+ they("should consider a negative value when delay:true is used with a $prop animation", {
+ 'transition': function() {
+ return {
+ prop: 'transition-delay',
+ css: 'transition:2s linear all; transition-delay: -1s'
+ };
+ },
+ 'keyframe': function(prefix) {
+ return {
+ prop: prefix + 'animation-delay',
+ css: prefix + 'animation:2s keyframe_animation; ' + prefix + 'animation-delay: -1s;'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = {
+ delay: true,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(testDetails.prop)).toContain('-1s');
+ });
+ });
+
+ they("should consider a negative value when a negative option delay is provided for a $prop animation", {
+ 'transition': function() {
+ return {
+ prop: 'transition-delay',
+ css: 'transition:2s linear all'
+ };
+ },
+ 'keyframe': function(prefix) {
+ return {
+ prop: prefix + 'animation-delay',
+ css: prefix + 'animation:2s keyframe_animation'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = {
+ delay: -2,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(testDetails.prop)).toContain('-2s');
+ });
+ });
+
+ they("should expect the $propend event to always return the full duration even when negative values are used", {
+ 'transition': function() {
+ return {
+ event: 'transitionend',
+ css: 'transition:5s linear all; transition-delay: -2s'
+ };
+ },
+ 'animation': function(prefix) {
+ return {
+ event: 'animationend',
+ css: prefix + 'animation:5s keyframe_animation; ' + prefix + 'animation-delay: -2s;'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+ var event = testDetails.event;
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = { event: 'enter', structural: true };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ // 5 + (-2s) = 3
+ browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 3 });
+
+ assertAnimationRunning(element, true);
+
+ // 5 seconds is the full animation
+ browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 5 });
+
+ assertAnimationRunning(element);
+ });
+ });
+ });
+
+ describe("[transtionStyle]", function() {
+ it("should apply the transition directly onto the element and animate accordingly",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s linear all',
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('5.5s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 5.5 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should give priority to the provided duration value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s ease-in color',
+ duration: 4,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('4s');
+ expect(element.css('transition-property')).toMatch('color');
+ expect(style).toContain('ease-in');
+ }));
+
+ it("should give priority to the provided delay value, but only update the delay style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s 4s ease-in color',
+ delay: 20,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(element.css('transition-delay')).toMatch('20s');
+ expect(element.css('transition-duration')).toMatch('5.5s');
+ expect(element.css('transition-property')).toMatch('color');
+ expect(style).toContain('ease-in');
+ }));
+ });
+
+ describe("[keyframeStyle]", function() {
+ it("should apply the keyframe animation directly onto the element and animate accordingly",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s',
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var detectedStyle = element.attr('style');
+ expect(detectedStyle).toContain('5.5s');
+ expect(detectedStyle).toContain('my_animation');
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 5.5 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should give priority to the provided duration value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s',
+ duration: 50,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var detectedStyle = element.attr('style');
+ expect(detectedStyle).toContain('50s');
+ expect(detectedStyle).toContain('my_animation');
+ }));
+
+ it("should give priority to the provided delay value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s 10s',
+ delay: 50,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('50s');
+ expect(element.css(prefix + 'animation-duration')).toEqual('5.5s');
+ expect(element.css(prefix + 'animation-name')).toEqual('my_animation');
+ }));
+ });
+
+ describe("[from] and [to]", function() {
+ it("should apply from styles to an element during the preparation phase",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 2.5,
+ event: 'enter',
+ structural: true,
+ from: { width: '50px' },
+ to: { width: '100px' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(element.attr('style')).toMatch(/width:\s*50px/);
+ }));
+
+ it("should apply to styles to an element during the animation phase",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 2.5,
+ event: 'enter',
+ structural: true,
+ from: { width: '15px' },
+ to: { width: '25px' }
+ };
+
+ var animator = $animateCss(element, options);
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+ runner.end();
+
+ expect(element.css('width')).toBe('25px');
+ }));
+
+ it("should apply the union of from and to styles to the element if no animation is run",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ from: { 'width': '10px', height: '50px' },
+ to: { 'width': '15px' }
+ };
+
+ var animator = $animateCss(element, options);
+
+ expect(animator).toBeFalsy();
+ expect(element.css('width')).toBe('15px');
+ expect(element.css('height')).toBe('50px');
+ }));
+
+ it("should retain to and from styles on an element after an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 10,
+ from: { 'width': '10px', height: '66px' },
+ to: { 'width': '5px' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 10 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element.css('width')).toBe('5px');
+ expect(element.css('height')).toBe('66px');
+ }));
+
+ it("should apply an inline transition if [to] styles and a duration are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5,
+ to: { background: 'red' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('2.5s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+
+ it("should remove all inline transition styling when an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5,
+ to: { background: 'red' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(style).toContain('transition');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 2500, elapsedTime: 2.5 });
+
+ style = element.attr('style');
+ expect(style).not.toContain('transition');
+ }));
+
+ it("should retain existing styles when an inline styled animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5
+ };
+
+ element.css('font-size', '20px');
+ element.css('opacity', '0.5');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('transition');
+ animator.end();
+
+ style = element.attr('style');
+ expect(element.attr('style')).not.toContain('transition');
+ expect(element.css('opacity')).toEqual('0.5');
+ }));
+
+ it("should remove all inline transition delay styling when an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition: 1s linear color');
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ delay: 5
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css('transition-delay')).toEqual('5s');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 1 });
+
+ expect(element.attr('style') || '').not.toContain('transition');
+ }));
+
+ it("should not apply an inline transition if only [from] styles and a duration are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 3,
+ from: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator).toBeFalsy();
+ }));
+
+ it("should apply a transition if [from] styles are provided with a class that is added",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ addClass: 'superb',
+ from: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(isFunction(animator.start)).toBe(true);
+ }));
+
+ it("should apply an inline transition if only [from] styles, but classes are added or removed and a duration is provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 3,
+ addClass: 'sugar',
+ from: { background: 'yellow' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator).toBeTruthy();
+ }));
+
+ it("should not apply an inline transition if no styles are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var emptyObject = {};
+ var options = {
+ duration: 3,
+ to: emptyObject,
+ from: emptyObject
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator).toBeFalsy();
+ }));
+
+ it("should apply a transition duration if the existing transition duration's property value is not 'all'",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition: 1s linear color');
+
+ var emptyObject = {};
+ var options = {
+ event: 'enter',
+ structural: true,
+ to: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('1s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+
+ it("should apply a transition duration and an animation duration if duration + styles options are provided for a matching keyframe animation",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3.5s keyframe_animation;' +
+ 'animation:3.5s keyframe_animation;');
+
+ var emptyObject = {};
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 10,
+ to: {
+ background: 'blue'
+ }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css('transition-duration')).toMatch('10s');
+ expect(element.css(prefix + 'animation-duration')).toEqual('10s');
+ }));
+ });
+
+ describe("[easing]", function() {
+
+ var element;
+ beforeEach(inject(function($document, $rootElement) {
+ element = jqLite('');
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+ }));
+
+ it("should apply easing to a transition animation if it exists", inject(function($animateCss) {
+ ss.addRule('.red', 'transition:1s linear all;');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('ease-out');
+ }));
+
+ it("should not apply easing to transitions nor keyframes on an element animation if nothing is detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.red', ';');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should apply easing to both keyframes and transition animations if detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.red', 'transition: 1s linear all;');
+ ss.addRule('.blue', prefix + 'animation:my_keyframe 1s;');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red blue', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toMatch(/animation(?:-timing-function)?:\s*ease-out/);
+ expect(style).toMatch(/transition(?:-timing-function)?:\s*ease-out/);
+ }));
+ });
+
+ it('should round up long elapsedTime values to close off a CSS3 animation',
+ inject(function($animateCss) {
+
+ ss.addRule('.millisecond-transition.ng-leave', '-webkit-transition:510ms linear all;' +
+ 'transition:510ms linear all;');
+
+ element.addClass('millisecond-transition');
+ var animator = $animateCss(element, {
+ event: 'leave',
+ structural: true
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-leave-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 0.50999999991 });
+
+ expect(element).not.toHaveClass('ng-leave-active');
+ }));
+ });
+
+ describe('SVG', function() {
+ it('should properly apply transitions on an SVG element',
+ inject(function($animateCss, $rootScope, $compile, $document, $rootElement) {
+
+ var element = $compile('')($rootScope);
+
+ jqLite($document[0].body).append($rootElement);
+ $rootElement.append(element);
+
+ $animateCss(element, {
+ event: 'enter',
+ structural: true,
+ duration: 10
+ }).start();
+
+ triggerAnimationStartFrame();
+
+ expect(jqLiteHasClass(element[0], 'ng-enter')).toBe(true);
+ expect(jqLiteHasClass(element[0], 'ng-enter-active')).toBe(true);
+
+ browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ expect(jqLiteHasClass(element[0], 'ng-enter')).toBe(false);
+ expect(jqLiteHasClass(element[0], 'ng-enter-active')).toBe(false);
+ }));
+
+ it('should properly remove classes from SVG elements', inject(function($animateCss) {
+ var element = jqLite('');
+ var child = element.find('rect');
+
+ $animateCss(child, {
+ removeClass: 'class-of-doom',
+ duration: 0
+ });
+
+ expect(child.attr('class')).toBe('');
+ }));
+ });
+ });
+});
diff --git a/test/ngAnimate/animateJsDriverSpec.js b/test/ngAnimate/animateJsDriverSpec.js
new file mode 100644
index 000000000000..274457965f10
--- /dev/null
+++ b/test/ngAnimate/animateJsDriverSpec.js
@@ -0,0 +1,178 @@
+'use strict';
+
+describe("ngAnimate $$animateJsDriver", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ it('should register the $$animateJsDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateJsDriver');
+ }));
+
+ describe('with $$animateJs', function() {
+ var capturedAnimation = null;
+ var captureLog = [];
+ var element;
+ var driver;
+
+ beforeEach(module(function($provide) {
+ $provide.factory('$$animateJs', function($$AnimateRunner) {
+ return function() {
+ var runner = new $$AnimateRunner();
+ capturedAnimation = arguments;
+ captureLog.push({
+ args: capturedAnimation,
+ runner: runner
+ });
+ return {
+ start: function() {
+ return runner;
+ }
+ };
+ };
+ });
+
+ captureLog.length = 0;
+ element = jqLite('');
+ element.append(child1);
+ var child2 = jqLite('');
+ element.append(child2);
+
+ driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+ $rootScope.$digest();
+
+ expect(captureLog.length).toBe(2);
+
+ var first = captureLog[0].args;
+ expect(first[0]).toBe(child1);
+ expect(first[1]).toBe('leave');
+
+ var second = captureLog[1].args;
+ expect(second[0]).toBe(child2);
+ expect(second[1]).toBe('enter');
+ }));
+
+ they('should $prop both animations when $prop() is called on the runner', ['end', 'cancel'], function(method) {
+ inject(function($rootScope, $$rAF) {
+ var child1 = jqLite('');
+ element.append(child1);
+ var child2 = jqLite('');
+ element.append(child2);
+
+ var animator = driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+
+ var runner = animator.start();
+
+ var animationsClosed = false;
+ var status;
+ runner.done(function(s) {
+ animationsClosed = true;
+ status = s;
+ });
+
+ $rootScope.$digest();
+
+ runner[method]();
+ $$rAF.flush();
+
+ expect(animationsClosed).toBe(true);
+ expect(status).toBe(method === 'end' ? true : false);
+ });
+ });
+
+ they('should fully $prop when all inner animations are complete', ['end', 'cancel'], function(method) {
+ inject(function($rootScope, $$rAF) {
+ var child1 = jqLite('');
+ element.append(child1);
+ var child2 = jqLite('');
+ element.append(child2);
+
+ var animator = driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+
+ var runner = animator.start();
+
+ var animationsClosed = false;
+ var status;
+ runner.done(function(s) {
+ animationsClosed = true;
+ status = s;
+ });
+
+ $$rAF.flush();
+
+ captureLog[0].runner[method]();
+ expect(animationsClosed).toBe(false);
+
+ captureLog[1].runner[method]();
+ expect(animationsClosed).toBe(true);
+
+ expect(status).toBe(method === 'end' ? true : false);
+ });
+ });
+ });
+});
diff --git a/test/ngAnimate/animateJsSpec.js b/test/ngAnimate/animateJsSpec.js
new file mode 100644
index 000000000000..63245b28e78d
--- /dev/null
+++ b/test/ngAnimate/animateJsSpec.js
@@ -0,0 +1,672 @@
+'use strict';
+
+describe("ngAnimate $$animateJs", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function getDoneFunction(args) {
+ for (var i = 1; i < args.length; i++) {
+ var a = args[i];
+ if (isFunction(a)) return a;
+ }
+ }
+
+ it('should return nothing if no animations are registered at all', inject(function($$animateJs) {
+ var element = jqLite('');
+ expect($$animateJs(element, 'enter')).toBeFalsy();
+ }));
+
+ it('should return nothing if no matching animations classes are found', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ expect($$animateJs(element, 'enter')).toBeFalsy();
+ });
+ });
+
+ it('should return nothing if a matching animation class is found, but not a matching event', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ expect($$animateJs(element, 'leave')).toBeFalsy();
+ });
+ });
+
+ it('should return a truthy value if a matching animation class and event are found', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ expect($$animateJs(element, 'enter')).toBeTruthy();
+ });
+ });
+
+ it('should strictly query for the animation based on the classes value if passed in', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.superman', function() {
+ return { enter: noop };
+ });
+ $animateProvider.register('.batman', function() {
+ return { leave: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ expect($$animateJs(element, 'enter', 'superman')).toBeTruthy();
+ expect($$animateJs(element, 'leave', 'legoman batman')).toBeTruthy();
+ expect($$animateJs(element, 'enter', 'legoman')).toBeFalsy();
+ expect($$animateJs(element, 'leave', {})).toBeTruthy();
+ });
+ });
+
+ it('should run multiple animations in parallel', function() {
+ var doneCallbacks = [];
+ function makeAnimation(event) {
+ return function() {
+ var data = {};
+ data[event] = function(element, done) {
+ doneCallbacks.push(done);
+ };
+ return data;
+ };
+ }
+ module(function($animateProvider) {
+ $animateProvider.register('.one', makeAnimation('enter'));
+ $animateProvider.register('.two', makeAnimation('enter'));
+ $animateProvider.register('.three', makeAnimation('enter'));
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'enter');
+ var complete = false;
+ animator.start().done(function() {
+ complete = true;
+ });
+ expect(doneCallbacks.length).toBe(3);
+ forEach(doneCallbacks, function(cb) {
+ cb();
+ });
+ $$rAF.flush();
+ expect(complete).toBe(true);
+ });
+ });
+
+ they('should $prop the animation when runner.$prop() is called', ['end', 'cancel'], function(method) {
+ var ended = false;
+ var status;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ ended = true;
+ status = cancelled ? 'cancel' : 'end';
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+
+ expect(ended).toBeFalsy();
+ runner[method]();
+ expect(ended).toBeTruthy();
+ expect(status).toBe(method);
+ });
+ });
+
+ they('should $prop all of the running the animations when runner.$prop() is called',
+ ['end', 'cancel'], function(method) {
+
+ var lookup = {};
+ module(function($animateProvider) {
+ forEach(['one','two','three'], function(klass) {
+ $animateProvider.register('.' + klass, function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ lookup[klass] = cancelled ? 'cancel' : 'end';
+ };
+ }
+ };
+ });
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ runner[method]();
+ expect(lookup.one).toBe(method);
+ expect(lookup.two).toBe(method);
+ expect(lookup.three).toBe(method);
+ });
+ });
+
+ they('should only run the $prop operation once', ['end', 'cancel'], function(method) {
+ var ended = false;
+ var count = 0;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ ended = true;
+ count++;
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+
+ expect(ended).toBeFalsy();
+ runner[method]();
+ expect(ended).toBeTruthy();
+ expect(count).toBe(1);
+
+ runner[method]();
+ expect(count).toBe(1);
+ });
+ });
+
+ it('should always run the provided animation in atleast one RAF frame if defined', function() {
+ var before, after, endCalled;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ beforeAddClass: function(element, className, done) {
+ before = done;
+ },
+ addClass: function(element, className, done) {
+ after = done;
+ }
+ };
+ });
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'addClass', {
+ addClass: 'red'
+ });
+
+ var runner = animator.start();
+ runner.done(function() {
+ endCalled = true;
+ });
+
+ expect(before).toBeDefined();
+ before();
+
+ expect(after).toBeUndefined();
+ $$rAF.flush();
+ expect(after).toBeDefined();
+ after();
+
+ expect(endCalled).toBeUndefined();
+ $$rAF.flush();
+ expect(endCalled).toBe(true);
+ });
+ });
+
+ they('should still run the associated DOM event when the $prop function is run but no more animations', ['cancel', 'end'], function(method) {
+ var log = [];
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ beforeAddClass: function() {
+ return function(cancelled) {
+ var status = cancelled ? 'cancel' : 'end';
+ log.push('before addClass ' + status);
+ };
+ },
+ addClass: function() {
+ return function(cancelled) {
+ var status = cancelled ? 'cancel' : 'end';
+ log.push('after addClass' + status);
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'addClass', {
+ domOperation: function() {
+ log.push('dom addClass');
+ }
+ });
+ var runner = animator.start();
+ runner.done(function() {
+ log.push('addClass complete');
+ });
+ runner[method]();
+
+ $$rAF.flush();
+ expect(log).toEqual(
+ ['before addClass ' + method,
+ 'dom addClass',
+ 'addClass complete']);
+ });
+ });
+
+ it('should resolve the promise when end() is called', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return { beforeAddClass: noop };
+ });
+ });
+ inject(function($$animateJs, $$rAF, $rootScope) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'addClass');
+ var runner = animator.start();
+ var done = false;
+ var cancelled = false;
+ runner.then(function() {
+ done = true;
+ }, function() {
+ cancelled = true;
+ });
+
+ runner.end();
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(true);
+ expect(cancelled).toBe(false);
+ });
+ });
+
+ it('should reject the promise when cancel() is called', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return { beforeAddClass: noop };
+ });
+ });
+ inject(function($$animateJs, $$rAF, $rootScope) {
+ var element = jqLite('');
+ var animator = $$animateJs(element, 'addClass');
+ var runner = animator.start();
+ var done = false;
+ var cancelled = false;
+ runner.then(function() {
+ done = true;
+ }, function() {
+ cancelled = true;
+ });
+
+ runner.cancel();
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(false);
+ expect(cancelled).toBe(true);
+ });
+ });
+
+ describe("events", function() {
+ var animations, runAnimation, element, log;
+ beforeEach(module(function($animateProvider) {
+ element = jqLite('');
+ animations = {};
+ log = [];
+
+ $animateProvider.register('.test-animation', function() {
+ return animations;
+ });
+
+ return function($$animateJs) {
+ runAnimation = function(method, done, error, options) {
+ options = extend(options || {}, {
+ domOperation: function() {
+ log.push('dom ' + method);
+ }
+ });
+
+ var driver = $$animateJs(element, method, 'test-animation', options);
+ driver.start().done(function(status) {
+ ((status ? done : error) || noop)();
+ });
+ };
+ };
+ }));
+
+ they("$prop should have the function signature of (element, done, options) for the after animation",
+ ['enter', 'move', 'leave'], function(event) {
+ inject(function() {
+ var args;
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animations[event] = function() {
+ args = arguments;
+ };
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(3);
+ expect(args[0]).toBe(element);
+ expect(isFunction(args[1])).toBe(true);
+ expect(args[2].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("$prop should not execute a before function", enterMoveEvents, function(event) {
+ inject(function() {
+ var args;
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ var animationOptions = {};
+ animations[beforeMethod] = function() {
+ args = arguments;
+ };
+
+ runAnimation(event, noop, noop, animationOptions);
+ expect(args).toBeFalsy();
+ });
+ });
+
+ they("$prop should have the function signature of (element, className, done, options) for the before animation",
+ ['addClass', 'removeClass'], function(event) {
+ inject(function() {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ var args;
+ var className = 'matias';
+ animations[beforeMethod] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animationOptions[event] = className;
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(4);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(className);
+ expect(isFunction(args[2])).toBe(true);
+ expect(args[3].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("$prop should have the function signature of (element, className, done, options) for the after animation",
+ ['addClass', 'removeClass'], function(event) {
+ inject(function() {
+ var args;
+ var className = 'fatias';
+ animations[event] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animationOptions[event] = className;
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(4);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(className);
+ expect(isFunction(args[2])).toBe(true);
+ expect(args[3].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("setClass should have the function signature of (element, addClass, removeClass, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeSetClass' : 'setClass';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var addClass = 'on';
+ var removeClass = 'on';
+ var animationOptions = {
+ foo: 'bar',
+ addClass: addClass,
+ removeClass: removeClass
+ };
+ runAnimation('setClass', noop, noop, animationOptions);
+
+ expect(args.length).toBe(5);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(addClass);
+ expect(args[2]).toBe(removeClass);
+ expect(isFunction(args[3])).toBe(true);
+ expect(args[4].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("animate should have the function signature of (element, from, to, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeAnimate' : 'animate';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var to = { color: 'red' };
+ var from = { color: 'blue' };
+ var animationOptions = {
+ foo: 'bar',
+ to: to,
+ from: from
+ };
+ runAnimation('animate', noop, noop, animationOptions);
+
+ expect(args.length).toBe(5);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(from);
+ expect(args[2]).toBe(to);
+ expect(isFunction(args[3])).toBe(true);
+ expect(args[4].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("custom events should have the function signature of (element, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeCustom' : 'custom';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ runAnimation('custom', noop, noop, animationOptions);
+
+ expect(args.length).toBe(3);
+ expect(args[0]).toBe(element);
+ expect(isFunction(args[1])).toBe(true);
+ expect(args[2].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ var enterMoveEvents = ['enter', 'move'];
+ var otherEvents = ['addClass', 'removeClass', 'setClass'];
+ var allEvents = ['leave'].concat(otherEvents).concat(enterMoveEvents);
+
+ they("$prop should asynchronously render the before$prop animation", otherEvents, function(event) {
+ inject(function($$rAF) {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ animations[beforeMethod] = function(element, a, b, c) {
+ log.push('before ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event);
+ expect(log).toEqual(['before ' + event]);
+ $$rAF.flush();
+
+ expect(log).toEqual(['before ' + event, 'dom ' + event]);
+ });
+ });
+
+ they("$prop should asynchronously render the $prop animation", allEvents, function(event) {
+ inject(function($$rAF) {
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event, function() {
+ log.push('complete');
+ });
+
+ if (event === 'leave') {
+ expect(log).toEqual(['after leave']);
+ $$rAF.flush();
+ expect(log).toEqual(['after leave', 'dom leave', 'complete']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'after ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'after ' + event, 'complete']);
+ }
+ });
+ });
+
+ they("$prop should asynchronously reject the before animation if the callback function is called with false", otherEvents, function(event) {
+ inject(function($$rAF, $rootScope) {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ animations[beforeMethod] = function(element, a, b, c) {
+ log.push('before ' + event);
+ var done = getDoneFunction(arguments);
+ done(false);
+ };
+
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event,
+ function() { log.push('pass'); },
+ function() { log.push('fail'); });
+
+ expect(log).toEqual(['before ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['before ' + event, 'dom ' + event, 'fail']);
+ });
+ });
+
+ they("$prop should asynchronously reject the after animation if the callback function is called with false", allEvents, function(event) {
+ inject(function($$rAF, $rootScope) {
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done(false);
+ };
+
+ runAnimation(event,
+ function() { log.push('pass'); },
+ function() { log.push('fail'); });
+
+ var expectations = [];
+ if (event === 'leave') {
+ expect(log).toEqual(['after leave']);
+ $$rAF.flush();
+ expect(log).toEqual(['after leave', 'dom leave', 'fail']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'after ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'after ' + event, 'fail']);
+ }
+ });
+ });
+
+ it('setClass should delegate down to addClass/removeClass if not defined', inject(function($$rAF) {
+ animations.addClass = function(element, done) {
+ log.push('addClass');
+ };
+
+ animations.removeClass = function(element, done) {
+ log.push('removeClass');
+ };
+
+ expect(animations.setClass).toBeFalsy();
+
+ runAnimation('setClass');
+
+ expect(log).toEqual(['dom setClass', 'removeClass', 'addClass']);
+ }));
+
+ it('beforeSetClass should delegate down to beforeAddClass/beforeRemoveClass if not defined',
+ inject(function($$rAF) {
+
+ animations.beforeAddClass = function(element, className, done) {
+ log.push('beforeAddClass');
+ done();
+ };
+
+ animations.beforeRemoveClass = function(element, className, done) {
+ log.push('beforeRemoveClass');
+ done();
+ };
+
+ expect(animations.setClass).toBeFalsy();
+
+ runAnimation('setClass');
+ $$rAF.flush();
+
+ expect(log).toEqual(['beforeRemoveClass', 'beforeAddClass', 'dom setClass']);
+ }));
+
+ it('leave should always ignore the `beforeLeave` animation',
+ inject(function($$rAF) {
+
+ animations.beforeLeave = function(element, done) {
+ log.push('beforeLeave');
+ done();
+ };
+
+ animations.leave = function(element, done) {
+ log.push('leave');
+ done();
+ };
+
+ runAnimation('leave');
+ $$rAF.flush();
+
+ expect(log).toEqual(['leave', 'dom leave']);
+ }));
+
+ it('should allow custom events to be triggered',
+ inject(function($$rAF) {
+
+ animations.beforeFlex = function(element, done) {
+ log.push('beforeFlex');
+ done();
+ };
+
+ animations.flex = function(element, done) {
+ log.push('flex');
+ done();
+ };
+
+ runAnimation('flex');
+ $$rAF.flush();
+
+ expect(log).toEqual(['beforeFlex', 'dom flex', 'flex']);
+ }));
+ });
+});
diff --git a/test/ngAnimate/animateRunnerSpec.js b/test/ngAnimate/animateRunnerSpec.js
new file mode 100644
index 000000000000..94178fba6189
--- /dev/null
+++ b/test/ngAnimate/animateRunnerSpec.js
@@ -0,0 +1,340 @@
+'use strict';
+
+describe('$$rAFMutex', function() {
+ beforeEach(module('ngAnimate'));
+
+ it('should fire the callback only when one or more RAFs have passed',
+ inject(function($$rAF, $$rAFMutex) {
+
+ var trigger = $$rAFMutex();
+ var called = false;
+ trigger(function() {
+ called = true;
+ });
+
+ expect(called).toBe(false);
+ $$rAF.flush();
+ expect(called).toBe(true);
+ }));
+
+ it('should immediately fire the callback if a RAF has passed since construction',
+ inject(function($$rAF, $$rAFMutex) {
+
+ var trigger = $$rAFMutex();
+ $$rAF.flush();
+
+ var called = false;
+ trigger(function() {
+ called = true;
+ });
+ expect(called).toBe(true);
+ }));
+});
+
+describe("$$AnimateRunner", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ they("should trigger the host $prop function",
+ ['end', 'cancel', 'pause', 'resume'], function(method) {
+
+ inject(function($$AnimateRunner) {
+ var host = {};
+ var spy = host[method] = jasmine.createSpy();
+ var runner = new $$AnimateRunner(host);
+ runner[method]();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ they("should trigger the inner runner's host $prop function",
+ ['end', 'cancel', 'pause', 'resume'], function(method) {
+
+ inject(function($$AnimateRunner) {
+ var host = {};
+ var spy = host[method] = jasmine.createSpy();
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner(host);
+ runner1.setHost(runner2);
+ runner1[method]();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ it("should resolve the done function only if one RAF has passed",
+ inject(function($$AnimateRunner, $$rAF) {
+
+ var runner = new $$AnimateRunner();
+ var spy = jasmine.createSpy();
+ runner.done(spy);
+ runner.complete(true);
+ expect(spy).not.toHaveBeenCalled();
+ $$rAF.flush();
+ expect(spy).toHaveBeenCalled();
+ }));
+
+ it("should resolve with the status provided in the completion function",
+ inject(function($$AnimateRunner, $$rAF) {
+
+ var runner = new $$AnimateRunner();
+ var capturedValue;
+ runner.done(function(val) {
+ capturedValue = val;
+ });
+ runner.complete('special value');
+ $$rAF.flush();
+ expect(capturedValue).toBe('special value');
+ }));
+
+ they("should immediately resolve each combined runner in a bottom-up order when $prop is called",
+ ['end', 'cancel'], function(method) {
+
+ inject(function($$AnimateRunner, $$rAF) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ runner1.setHost(runner2);
+
+ var status1, status2, signature = '';
+ runner1.done(function(status) {
+ signature += '1';
+ status1 = status;
+ });
+
+ runner2.done(function(status) {
+ signature += '2';
+ status2 = status;
+ });
+
+ runner1[method]();
+
+ var expectedStatus = method === 'end' ? true : false;
+ expect(status1).toBe(expectedStatus);
+ expect(status2).toBe(expectedStatus);
+ expect(signature).toBe('21');
+ });
+ });
+
+ they("should resolve/reject using a newly created promise when .then() is used upon $prop",
+ ['end', 'cancel'], function(method) {
+
+ inject(function($$AnimateRunner, $rootScope) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ runner1.setHost(runner2);
+
+ var status1;
+ runner1.then(
+ function() { status1 = 'pass'; },
+ function() { status1 = 'fail'; });
+
+ var status2;
+ runner2.then(
+ function() { status2 = 'pass'; },
+ function() { status2 = 'fail'; });
+
+ runner1[method]();
+
+ var expectedStatus = method === 'end' ? 'pass' : 'fail';
+
+ expect(status1).toBeUndefined();
+ expect(status2).toBeUndefined();
+
+ $rootScope.$digest();
+ expect(status1).toBe(expectedStatus);
+ expect(status2).toBe(expectedStatus);
+ });
+ });
+
+ it("should expose/create the contained promise when getPromise() is called",
+ inject(function($$AnimateRunner, $rootScope) {
+
+ var runner = new $$AnimateRunner();
+ expect(isPromiseLike(runner.getPromise())).toBeTruthy();
+ }));
+
+ it("should expose the `catch` promise function to handle the rejected state",
+ inject(function($$AnimateRunner, $rootScope) {
+
+ var runner = new $$AnimateRunner();
+ var animationFailed = false;
+ runner.catch(function() {
+ animationFailed = true;
+ });
+ runner.cancel();
+ $rootScope.$digest();
+ expect(animationFailed).toBe(true);
+ }));
+
+ they("should expose the `finally` promise function to handle the final state when $prop",
+ { 'rejected': 'cancel', 'resolved': 'end' }, function(method) {
+ inject(function($$AnimateRunner, $rootScope) {
+ var runner = new $$AnimateRunner();
+ var animationComplete = false;
+ runner.finally(function() {
+ animationComplete = true;
+ });
+ runner[method]();
+ $rootScope.$digest();
+ expect(animationComplete).toBe(true);
+ });
+ });
+
+ describe(".all()", function() {
+ it("should resolve when all runners have naturally resolved",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1.complete(true);
+ runner2.complete(true);
+ runner3.complete(true);
+
+ expect(status).toBeUndefined();
+
+ $$rAF.flush();
+
+ expect(status).toBe(true);
+ }));
+
+ they("should immediately resolve if and when all runners have been $prop",
+ { ended: 'end', cancelled: 'cancel' }, function(method) {
+
+ inject(function($$rAF, $$AnimateRunner) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var expectedStatus = method === 'end' ? true : false;
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1[method]();
+ runner2[method]();
+ runner3[method]();
+
+ expect(status).toBe(expectedStatus);
+ });
+ });
+
+ it("should return a status of `false` if one or more runners was cancelled",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1.end();
+ runner2.end();
+ runner3.cancel();
+
+ expect(status).toBe(false);
+ }));
+ });
+
+ describe(".chain()", function() {
+ it("should evaluate an array of functions in a chain",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var log = [];
+
+ var items = [];
+ items.push(function(fn) {
+ runner1.done(function() {
+ log.push(1);
+ fn();
+ });
+ });
+
+ items.push(function(fn) {
+ runner2.done(function() {
+ log.push(2);
+ fn();
+ });
+ });
+
+ items.push(function(fn) {
+ runner3.done(function() {
+ log.push(3);
+ fn();
+ });
+ });
+
+ var status;
+ $$AnimateRunner.chain(items, function(response) {
+ status = response;
+ });
+
+ $$rAF.flush();
+
+ runner2.complete(true);
+ expect(log).toEqual([]);
+ expect(status).toBeUndefined();
+
+ runner1.complete(true);
+ expect(log).toEqual([1,2]);
+ expect(status).toBeUndefined();
+
+ runner3.complete(true);
+ expect(log).toEqual([1,2,3]);
+ expect(status).toBe(true);
+ }));
+
+ it("should break the chian when a function evaluates to false",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+ var runner4 = new $$AnimateRunner();
+ var runner5 = new $$AnimateRunner();
+ var runner6 = new $$AnimateRunner();
+
+ var log = [];
+
+ var items = [];
+ items.push(function(fn) { log.push(1); runner1.done(fn); });
+ items.push(function(fn) { log.push(2); runner2.done(fn); });
+ items.push(function(fn) { log.push(3); runner3.done(fn); });
+ items.push(function(fn) { log.push(4); runner4.done(fn); });
+ items.push(function(fn) { log.push(5); runner5.done(fn); });
+ items.push(function(fn) { log.push(6); runner6.done(fn); });
+
+ var status;
+ $$AnimateRunner.chain(items, function(response) {
+ status = response;
+ });
+
+ runner1.complete('');
+ runner2.complete(null);
+ runner3.complete(undefined);
+ runner4.complete(0);
+ runner5.complete(false);
+
+ runner6.complete(true);
+
+ $$rAF.flush();
+
+ expect(log).toEqual([1,2,3,4,5]);
+ expect(status).toBe(false);
+ }));
+ });
+});
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index be5a1b59e27e..5d89274585c4 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -1,5578 +1,1341 @@
'use strict';
-describe("ngAnimate", function() {
- var $originalAnimate;
- beforeEach(module(function($provide) {
- $provide.decorator('$animate', function($delegate) {
- $originalAnimate = $delegate;
- return $delegate;
- });
- }));
- beforeEach(module('ngAnimate'));
- beforeEach(module('ngAnimateMock'));
-
- function getMaxValue(prop, element, $window) {
- var node = element[0];
- var cs = $window.getComputedStyle(node);
- var prop0 = 'webkit' + prop.charAt(0).toUpperCase() + prop.substr(1);
- var values = (cs[prop0] || cs[prop]).split(/\s*,\s*/);
- var maxDelay = 0;
- forEach(values, function(value) {
- maxDelay = Math.max(parseFloat(value) || 0, maxDelay);
- });
- return maxDelay;
- }
-
- it("should disable animations on bootstrap for structural animations even after the first digest has passed", function() {
- var hasBeenAnimated = false;
- module(function($animateProvider) {
- $animateProvider.register('.my-structrual-animation', function() {
- return {
- enter: function(element, done) {
- hasBeenAnimated = true;
- done();
- },
- leave: function(element, done) {
- hasBeenAnimated = true;
- done();
- }
- };
- });
- });
- inject(function($rootScope, $compile, $animate, $rootElement, $document) {
- var element = $compile('
...
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+describe("animations", function() {
- $animate.enter(element, $rootElement);
- $rootScope.$digest();
-
- expect(hasBeenAnimated).toBe(false);
+ beforeEach(module('ngAnimate'));
- $animate.leave(element);
- $rootScope.$digest();
+ var element, applyAnimationClasses;
+ afterEach(inject(function($$jqLite) {
+ applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ dealoc(element);
+ }));
- expect(hasBeenAnimated).toBe(true);
- });
- });
+ describe('during bootstrap', function() {
+ it('should be enabled only after the first digest is fired and the postDigest queue is empty',
+ inject(function($animate, $rootScope) {
- it("should disable animations for two digests until all pending HTTP requests are complete during bootstrap", function() {
- var animateSpy = jasmine.createSpy();
- module(function($animateProvider, $compileProvider) {
- $compileProvider.directive('myRemoteDirective', function() {
- return {
- templateUrl: 'remote.html'
- };
- });
- $animateProvider.register('.my-structrual-animation', function() {
- return {
- enter: animateSpy,
- leave: animateSpy
- };
+ var capturedEnabledState;
+ $rootScope.$$postDigest(function() {
+ capturedEnabledState = $animate.enabled();
});
- });
- inject(function($rootScope, $compile, $animate, $rootElement, $document, $httpBackend) {
-
- $httpBackend.whenGET('remote.html').respond(200, 'content');
- var element = $compile('
...
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
-
- // running this twice just to prove that the dual post digest is run
- $rootScope.$digest();
+ expect($animate.enabled()).toBe(false);
$rootScope.$digest();
- $animate.enter(element, $rootElement);
- $rootScope.$digest();
+ expect(capturedEnabledState).toBe(false);
+ expect($animate.enabled()).toBe(true);
+ }));
- expect(animateSpy).not.toHaveBeenCalled();
+ it('should be disabled until all pending template requests have been downloaded', function() {
+ var mockTemplateRequest = {
+ totalPendingRequests: 2
+ };
- $httpBackend.flush();
- $rootScope.$digest();
+ module(function($provide) {
+ $provide.value('$templateRequest', mockTemplateRequest);
+ });
+ inject(function($animate, $rootScope) {
+ expect($animate.enabled()).toBe(false);
- $animate.leave(element);
- $rootScope.$digest();
+ $rootScope.$digest();
+ expect($animate.enabled()).toBe(false);
- expect(animateSpy).toHaveBeenCalled();
+ mockTemplateRequest.totalPendingRequests = 0;
+ $rootScope.$digest();
+ expect($animate.enabled()).toBe(true);
+ });
});
- });
+ it('should stay disabled if set to be disabled even after all templates have been fully downloaded', function() {
+ var mockTemplateRequest = {
+ totalPendingRequests: 2
+ };
- //we use another describe block because the before/after operations below
- //are used across all animations tests and we don't want that same behavior
- //to be used on the root describe block at the start of the animateSpec.js file
- describe('', function() {
+ module(function($provide) {
+ $provide.value('$templateRequest', mockTemplateRequest);
+ });
+ inject(function($animate, $rootScope) {
+ $animate.enabled(false);
+ expect($animate.enabled()).toBe(false);
- var ss, body;
- beforeEach(module(function() {
- body = jqLite(document.body);
- return function($window, $document, $animate, $timeout, $rootScope) {
- ss = createMockStyleSheet($document, $window);
- try {
- $timeout.flush();
- } catch (e) {}
- $animate.enabled(true);
$rootScope.$digest();
- };
- }));
+ expect($animate.enabled()).toBe(false);
- afterEach(function() {
- if (ss) {
- ss.destroy();
- }
- dealoc(body);
+ mockTemplateRequest.totalPendingRequests = 0;
+ $rootScope.$digest();
+ expect($animate.enabled()).toBe(false);
+ });
});
+ });
+ describe('$animate', function() {
+ var parent;
+ var parent2;
+ var options;
+ var capturedAnimation;
+ var capturedAnimationHistory;
+ var overriddenAnimationRunner;
+ var defaultFakeAnimationRunner;
- describe("$animate", function() {
+ beforeEach(module(function($provide) {
+ overriddenAnimationRunner = null;
+ capturedAnimation = null;
+ capturedAnimationHistory = [];
- var element, $rootElement;
+ options = {};
+ $provide.value('$$animation', function() {
+ capturedAnimationHistory.push(capturedAnimation = arguments);
+ return overriddenAnimationRunner || defaultFakeAnimationRunner;
+ });
- function html(content) {
- body.append($rootElement);
- $rootElement.html(content);
- element = $rootElement.children().eq(0);
- return element;
- }
+ return function($document, $rootElement, $q, $animate, $$AnimateRunner) {
+ defaultFakeAnimationRunner = new $$AnimateRunner();
+ $animate.enabled(true);
- describe("enable / disable", function() {
+ element = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- expect(element.hasClass('on')).toBe(false);
- expect(element.hasClass('off')).toBe(true);
-
- var signature = '';
- $animate.setClass(element, 'on', 'off').then(function() {
- signature += 'Z';
- });
- $rootScope.$digest();
-
- $animate.triggerReflow();
- $animate.triggerCallbackPromise();
-
- expect(signature).toBe('Z');
- expect(element.hasClass('on')).toBe(true);
- expect(element.hasClass('off')).toBe(false);
- }));
-
- it('should fire DOM callbacks on the element being animated',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.klass-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- var element = jqLite('');
- $rootElement.append(element);
- body.append($rootElement);
-
- var steps = [];
- element.on('$animate:before', function(e, data) {
- steps.push(['before', data.className, data.event]);
- });
-
- element.on('$animate:after', function(e, data) {
- steps.push(['after', data.className, data.event]);
- });
-
- element.on('$animate:close', function(e, data) {
- steps.push(['close', data.className, data.event]);
- });
-
- $animate.addClass(element, 'klass').then(function() {
- steps.push(['done', 'klass', 'addClass']);
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.pop()).toEqual(['before', 'klass', 'addClass']);
-
- $animate.triggerReflow();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.pop()).toEqual(['after', 'klass', 'addClass']);
-
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
-
- $animate.triggerCallbackEvents();
-
- expect(steps.shift()).toEqual(['close', 'klass', 'addClass']);
-
- $animate.triggerCallbackPromise();
-
- expect(steps.shift()).toEqual(['done', 'klass', 'addClass']);
- }));
-
- it('should fire the DOM callbacks even if no animation is rendered',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- $animate.enabled(true);
-
- var parent = jqLite('');
- var element = jqLite('');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var steps = [];
- element.on('$animate:before', function(e, data) {
- steps.push(['before', data.className, data.event]);
- });
-
- element.on('$animate:after', function(e, data) {
- steps.push(['after', data.className, data.event]);
- });
-
- $animate.enter(element, parent);
- $rootScope.$digest();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.shift()).toEqual(['before', 'ng-enter', 'enter']);
- expect(steps.shift()).toEqual(['after', 'ng-enter', 'enter']);
- }));
-
- it('should not fire DOM callbacks on the element being animated unless registered',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) {
-
- $animate.enabled(true);
-
- var element = jqLite('');
- $rootElement.append(element);
- body.append($rootElement);
-
- $animate.addClass(element, 'class');
- $rootScope.$digest();
-
- $timeout.verifyNoPendingTasks();
- }));
-
- it("should fire a done callback when provided with no animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var flag = false;
- $animate.removeClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire a done callback when provided with a css animation/transition",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- ss.addRule('.ng-hide-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
- ss.addRule('.ng-hide-remove', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = parent.find('span');
-
- var flag = false;
- $animate.addClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire a done callback when provided with a JS animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = parent.find('span');
- element.addClass('custom');
-
- var flag = false;
- $animate.removeClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire the callback right away if another animation is called right after",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- ss.addRule('.ng-hide-add', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
- ss.addRule('.ng-hide-remove', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
-
- var parent = jqLite('