blob: 30610044a9d243999c5cff33dc031689a2b846bb [file] [log] [blame]
Matteo Scandolo7cd88ba2015-12-16 14:23:08 -08001/**
2 * @license AngularJS v1.4.8
3 * (c) 2010-2015 Google, Inc. http://angularjs.org
4 * License: MIT
5 */
6(function(window, angular, undefined) {'use strict';
7
8/* jshint ignore:start */
9var noop = angular.noop;
10var extend = angular.extend;
11var jqLite = angular.element;
12var forEach = angular.forEach;
13var isArray = angular.isArray;
14var isString = angular.isString;
15var isObject = angular.isObject;
16var isUndefined = angular.isUndefined;
17var isDefined = angular.isDefined;
18var isFunction = angular.isFunction;
19var isElement = angular.isElement;
20
21var ELEMENT_NODE = 1;
22var COMMENT_NODE = 8;
23
24var ADD_CLASS_SUFFIX = '-add';
25var REMOVE_CLASS_SUFFIX = '-remove';
26var EVENT_CLASS_PREFIX = 'ng-';
27var ACTIVE_CLASS_SUFFIX = '-active';
28
29var NG_ANIMATE_CLASSNAME = 'ng-animate';
30var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren';
31
32// Detect proper transitionend/animationend event names.
33var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT;
34
35// If unprefixed events are not supported but webkit-prefixed are, use the latter.
36// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them.
37// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend`
38// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`.
39// Register both events in case `window.onanimationend` is not supported because of that,
40// do the same for `transitionend` as Safari is likely to exhibit similar behavior.
41// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit
42// therefore there is no reason to test anymore for other vendor prefixes:
43// http://caniuse.com/#search=transition
44if (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) {
45 CSS_PREFIX = '-webkit-';
46 TRANSITION_PROP = 'WebkitTransition';
47 TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
48} else {
49 TRANSITION_PROP = 'transition';
50 TRANSITIONEND_EVENT = 'transitionend';
51}
52
53if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) {
54 CSS_PREFIX = '-webkit-';
55 ANIMATION_PROP = 'WebkitAnimation';
56 ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend';
57} else {
58 ANIMATION_PROP = 'animation';
59 ANIMATIONEND_EVENT = 'animationend';
60}
61
62var DURATION_KEY = 'Duration';
63var PROPERTY_KEY = 'Property';
64var DELAY_KEY = 'Delay';
65var TIMING_KEY = 'TimingFunction';
66var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount';
67var ANIMATION_PLAYSTATE_KEY = 'PlayState';
68var SAFE_FAST_FORWARD_DURATION_VALUE = 9999;
69
70var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY;
71var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY;
72var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY;
73var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY;
74
75var isPromiseLike = function(p) {
76 return p && p.then ? true : false;
77};
78
79function assertArg(arg, name, reason) {
80 if (!arg) {
81 throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
82 }
83 return arg;
84}
85
86function mergeClasses(a,b) {
87 if (!a && !b) return '';
88 if (!a) return b;
89 if (!b) return a;
90 if (isArray(a)) a = a.join(' ');
91 if (isArray(b)) b = b.join(' ');
92 return a + ' ' + b;
93}
94
95function packageStyles(options) {
96 var styles = {};
97 if (options && (options.to || options.from)) {
98 styles.to = options.to;
99 styles.from = options.from;
100 }
101 return styles;
102}
103
104function pendClasses(classes, fix, isPrefix) {
105 var className = '';
106 classes = isArray(classes)
107 ? classes
108 : classes && isString(classes) && classes.length
109 ? classes.split(/\s+/)
110 : [];
111 forEach(classes, function(klass, i) {
112 if (klass && klass.length > 0) {
113 className += (i > 0) ? ' ' : '';
114 className += isPrefix ? fix + klass
115 : klass + fix;
116 }
117 });
118 return className;
119}
120
121function removeFromArray(arr, val) {
122 var index = arr.indexOf(val);
123 if (val >= 0) {
124 arr.splice(index, 1);
125 }
126}
127
128function stripCommentsFromElement(element) {
129 if (element instanceof jqLite) {
130 switch (element.length) {
131 case 0:
132 return [];
133 break;
134
135 case 1:
136 // there is no point of stripping anything if the element
137 // is the only element within the jqLite wrapper.
138 // (it's important that we retain the element instance.)
139 if (element[0].nodeType === ELEMENT_NODE) {
140 return element;
141 }
142 break;
143
144 default:
145 return jqLite(extractElementNode(element));
146 break;
147 }
148 }
149
150 if (element.nodeType === ELEMENT_NODE) {
151 return jqLite(element);
152 }
153}
154
155function extractElementNode(element) {
156 if (!element[0]) return element;
157 for (var i = 0; i < element.length; i++) {
158 var elm = element[i];
159 if (elm.nodeType == ELEMENT_NODE) {
160 return elm;
161 }
162 }
163}
164
165function $$addClass($$jqLite, element, className) {
166 forEach(element, function(elm) {
167 $$jqLite.addClass(elm, className);
168 });
169}
170
171function $$removeClass($$jqLite, element, className) {
172 forEach(element, function(elm) {
173 $$jqLite.removeClass(elm, className);
174 });
175}
176
177function applyAnimationClassesFactory($$jqLite) {
178 return function(element, options) {
179 if (options.addClass) {
180 $$addClass($$jqLite, element, options.addClass);
181 options.addClass = null;
182 }
183 if (options.removeClass) {
184 $$removeClass($$jqLite, element, options.removeClass);
185 options.removeClass = null;
186 }
187 }
188}
189
190function prepareAnimationOptions(options) {
191 options = options || {};
192 if (!options.$$prepared) {
193 var domOperation = options.domOperation || noop;
194 options.domOperation = function() {
195 options.$$domOperationFired = true;
196 domOperation();
197 domOperation = noop;
198 };
199 options.$$prepared = true;
200 }
201 return options;
202}
203
204function applyAnimationStyles(element, options) {
205 applyAnimationFromStyles(element, options);
206 applyAnimationToStyles(element, options);
207}
208
209function applyAnimationFromStyles(element, options) {
210 if (options.from) {
211 element.css(options.from);
212 options.from = null;
213 }
214}
215
216function applyAnimationToStyles(element, options) {
217 if (options.to) {
218 element.css(options.to);
219 options.to = null;
220 }
221}
222
223function mergeAnimationOptions(element, target, newOptions) {
224 var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || '');
225 var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || '');
226 var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove);
227
228 if (newOptions.preparationClasses) {
229 target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses);
230 delete newOptions.preparationClasses;
231 }
232
233 // noop is basically when there is no callback; otherwise something has been set
234 var realDomOperation = target.domOperation !== noop ? target.domOperation : null;
235
236 extend(target, newOptions);
237
238 // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this.
239 if (realDomOperation) {
240 target.domOperation = realDomOperation;
241 }
242
243 if (classes.addClass) {
244 target.addClass = classes.addClass;
245 } else {
246 target.addClass = null;
247 }
248
249 if (classes.removeClass) {
250 target.removeClass = classes.removeClass;
251 } else {
252 target.removeClass = null;
253 }
254
255 return target;
256}
257
258function resolveElementClasses(existing, toAdd, toRemove) {
259 var ADD_CLASS = 1;
260 var REMOVE_CLASS = -1;
261
262 var flags = {};
263 existing = splitClassesToLookup(existing);
264
265 toAdd = splitClassesToLookup(toAdd);
266 forEach(toAdd, function(value, key) {
267 flags[key] = ADD_CLASS;
268 });
269
270 toRemove = splitClassesToLookup(toRemove);
271 forEach(toRemove, function(value, key) {
272 flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS;
273 });
274
275 var classes = {
276 addClass: '',
277 removeClass: ''
278 };
279
280 forEach(flags, function(val, klass) {
281 var prop, allow;
282 if (val === ADD_CLASS) {
283 prop = 'addClass';
284 allow = !existing[klass];
285 } else if (val === REMOVE_CLASS) {
286 prop = 'removeClass';
287 allow = existing[klass];
288 }
289 if (allow) {
290 if (classes[prop].length) {
291 classes[prop] += ' ';
292 }
293 classes[prop] += klass;
294 }
295 });
296
297 function splitClassesToLookup(classes) {
298 if (isString(classes)) {
299 classes = classes.split(' ');
300 }
301
302 var obj = {};
303 forEach(classes, function(klass) {
304 // sometimes the split leaves empty string values
305 // incase extra spaces were applied to the options
306 if (klass.length) {
307 obj[klass] = true;
308 }
309 });
310 return obj;
311 }
312
313 return classes;
314}
315
316function getDomNode(element) {
317 return (element instanceof angular.element) ? element[0] : element;
318}
319
320function applyGeneratedPreparationClasses(element, event, options) {
321 var classes = '';
322 if (event) {
323 classes = pendClasses(event, EVENT_CLASS_PREFIX, true);
324 }
325 if (options.addClass) {
326 classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX));
327 }
328 if (options.removeClass) {
329 classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX));
330 }
331 if (classes.length) {
332 options.preparationClasses = classes;
333 element.addClass(classes);
334 }
335}
336
337function clearGeneratedClasses(element, options) {
338 if (options.preparationClasses) {
339 element.removeClass(options.preparationClasses);
340 options.preparationClasses = null;
341 }
342 if (options.activeClasses) {
343 element.removeClass(options.activeClasses);
344 options.activeClasses = null;
345 }
346}
347
348function blockTransitions(node, duration) {
349 // we use a negative delay value since it performs blocking
350 // yet it doesn't kill any existing transitions running on the
351 // same element which makes this safe for class-based animations
352 var value = duration ? '-' + duration + 's' : '';
353 applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
354 return [TRANSITION_DELAY_PROP, value];
355}
356
357function blockKeyframeAnimations(node, applyBlock) {
358 var value = applyBlock ? 'paused' : '';
359 var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
360 applyInlineStyle(node, [key, value]);
361 return [key, value];
362}
363
364function applyInlineStyle(node, styleTuple) {
365 var prop = styleTuple[0];
366 var value = styleTuple[1];
367 node.style[prop] = value;
368}
369
370function concatWithSpace(a,b) {
371 if (!a) return b;
372 if (!b) return a;
373 return a + ' ' + b;
374}
375
376var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) {
377 var queue, cancelFn;
378
379 function scheduler(tasks) {
380 // we make a copy since RAFScheduler mutates the state
381 // of the passed in array variable and this would be difficult
382 // to track down on the outside code
383 queue = queue.concat(tasks);
384 nextTick();
385 }
386
387 queue = scheduler.queue = [];
388
389 /* waitUntilQuiet does two things:
390 * 1. It will run the FINAL `fn` value only when an uncancelled RAF has passed through
391 * 2. It will delay the next wave of tasks from running until the quiet `fn` has run.
392 *
393 * The motivation here is that animation code can request more time from the scheduler
394 * before the next wave runs. This allows for certain DOM properties such as classes to
395 * be resolved in time for the next animation to run.
396 */
397 scheduler.waitUntilQuiet = function(fn) {
398 if (cancelFn) cancelFn();
399
400 cancelFn = $$rAF(function() {
401 cancelFn = null;
402 fn();
403 nextTick();
404 });
405 };
406
407 return scheduler;
408
409 function nextTick() {
410 if (!queue.length) return;
411
412 var items = queue.shift();
413 for (var i = 0; i < items.length; i++) {
414 items[i]();
415 }
416
417 if (!cancelFn) {
418 $$rAF(function() {
419 if (!cancelFn) nextTick();
420 });
421 }
422 }
423}];
424
425var $$AnimateChildrenDirective = [function() {
426 return function(scope, element, attrs) {
427 var val = attrs.ngAnimateChildren;
428 if (angular.isString(val) && val.length === 0) { //empty attribute
429 element.data(NG_ANIMATE_CHILDREN_DATA, true);
430 } else {
431 attrs.$observe('ngAnimateChildren', function(value) {
432 value = value === 'on' || value === 'true';
433 element.data(NG_ANIMATE_CHILDREN_DATA, value);
434 });
435 }
436 };
437}];
438
439var ANIMATE_TIMER_KEY = '$$animateCss';
440
441/**
442 * @ngdoc service
443 * @name $animateCss
444 * @kind object
445 *
446 * @description
447 * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes
448 * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT
449 * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or
450 * directives to create more complex animations that can be purely driven using CSS code.
451 *
452 * Note that only browsers that support CSS transitions and/or keyframe animations are capable of
453 * rendering animations triggered via `$animateCss` (bad news for IE9 and lower).
454 *
455 * ## Usage
456 * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that
457 * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however,
458 * any automatic control over cancelling animations and/or preventing animations from being run on
459 * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to
460 * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger
461 * the CSS animation.
462 *
463 * The example below shows how we can create a folding animation on an element using `ng-if`:
464 *
465 * ```html
466 * <!-- notice the `fold-animation` CSS class -->
467 * <div ng-if="onOff" class="fold-animation">
468 * This element will go BOOM
469 * </div>
470 * <button ng-click="onOff=true">Fold In</button>
471 * ```
472 *
473 * Now we create the **JavaScript animation** that will trigger the CSS transition:
474 *
475 * ```js
476 * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
477 * return {
478 * enter: function(element, doneFn) {
479 * var height = element[0].offsetHeight;
480 * return $animateCss(element, {
481 * from: { height:'0px' },
482 * to: { height:height + 'px' },
483 * duration: 1 // one second
484 * });
485 * }
486 * }
487 * }]);
488 * ```
489 *
490 * ## More Advanced Uses
491 *
492 * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks
493 * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code.
494 *
495 * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation,
496 * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with
497 * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order
498 * to provide a working animation that will run in CSS.
499 *
500 * The example below showcases a more advanced version of the `.fold-animation` from the example above:
501 *
502 * ```js
503 * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) {
504 * return {
505 * enter: function(element, doneFn) {
506 * var height = element[0].offsetHeight;
507 * return $animateCss(element, {
508 * addClass: 'red large-text pulse-twice',
509 * easing: 'ease-out',
510 * from: { height:'0px' },
511 * to: { height:height + 'px' },
512 * duration: 1 // one second
513 * });
514 * }
515 * }
516 * }]);
517 * ```
518 *
519 * Since we're adding/removing CSS classes then the CSS transition will also pick those up:
520 *
521 * ```css
522 * /&#42; since a hardcoded duration value of 1 was provided in the JavaScript animation code,
523 * the CSS classes below will be transitioned despite them being defined as regular CSS classes &#42;/
524 * .red { background:red; }
525 * .large-text { font-size:20px; }
526 *
527 * /&#42; we can also use a keyframe animation and $animateCss will make it work alongside the transition &#42;/
528 * .pulse-twice {
529 * animation: 0.5s pulse linear 2;
530 * -webkit-animation: 0.5s pulse linear 2;
531 * }
532 *
533 * @keyframes pulse {
534 * from { transform: scale(0.5); }
535 * to { transform: scale(1.5); }
536 * }
537 *
538 * @-webkit-keyframes pulse {
539 * from { -webkit-transform: scale(0.5); }
540 * to { -webkit-transform: scale(1.5); }
541 * }
542 * ```
543 *
544 * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen.
545 *
546 * ## How the Options are handled
547 *
548 * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation
549 * 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
550 * styles using the `from` and `to` properties.
551 *
552 * ```js
553 * var animator = $animateCss(element, {
554 * from: { background:'red' },
555 * to: { background:'blue' }
556 * });
557 * animator.start();
558 * ```
559 *
560 * ```css
561 * .rotating-animation {
562 * animation:0.5s rotate linear;
563 * -webkit-animation:0.5s rotate linear;
564 * }
565 *
566 * @keyframes rotate {
567 * from { transform: rotate(0deg); }
568 * to { transform: rotate(360deg); }
569 * }
570 *
571 * @-webkit-keyframes rotate {
572 * from { -webkit-transform: rotate(0deg); }
573 * to { -webkit-transform: rotate(360deg); }
574 * }
575 * ```
576 *
577 * 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
578 * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition
579 * 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
580 * 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
581 * and spread across the transition and keyframe animation.
582 *
583 * ## What is returned
584 *
585 * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually
586 * 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
587 * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties:
588 *
589 * ```js
590 * var animator = $animateCss(element, { ... });
591 * ```
592 *
593 * Now what do the contents of our `animator` variable look like:
594 *
595 * ```js
596 * {
597 * // starts the animation
598 * start: Function,
599 *
600 * // ends (aborts) the animation
601 * end: Function
602 * }
603 * ```
604 *
605 * 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.
606 * 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
607 * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties
608 * and that changing them will not reconfigure the parameters of the animation.
609 *
610 * ### runner.done() vs runner.then()
611 * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the
612 * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**.
613 * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()`
614 * unless you really need a digest to kick off afterwards.
615 *
616 * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss
617 * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code).
618 * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works.
619 *
620 * @param {DOMElement} element the element that will be animated
621 * @param {object} options the animation-related options that will be applied during the animation
622 *
623 * * `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
624 * 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.)
625 * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and
626 * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted.
627 * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
628 * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`).
629 * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
630 * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation.
631 * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition.
632 * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation.
633 * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation.
634 * * `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`
635 * is provided then the animation will be skipped entirely.
636 * * `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
637 * 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
638 * 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
639 * CSS delay value.
640 * * `stagger` - A numeric time value representing the delay between successively animated elements
641 * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.})
642 * * `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
643 * * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`)
644 * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.)
645 * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once
646 * the animation is closed. This is useful for when the styles are used purely for the sake of
647 * the animation and do not have a lasting visual effect on the element (e.g. a colapse and open animation).
648 * By default this value is set to `false`.
649 *
650 * @return {object} an object with start and end methods and details about the animation.
651 *
652 * * `start` - The method to start the animation. This will return a `Promise` when called.
653 * * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
654 */
655var ONE_SECOND = 1000;
656var BASE_TEN = 10;
657
658var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
659var CLOSING_TIME_BUFFER = 1.5;
660
661var DETECT_CSS_PROPERTIES = {
662 transitionDuration: TRANSITION_DURATION_PROP,
663 transitionDelay: TRANSITION_DELAY_PROP,
664 transitionProperty: TRANSITION_PROP + PROPERTY_KEY,
665 animationDuration: ANIMATION_DURATION_PROP,
666 animationDelay: ANIMATION_DELAY_PROP,
667 animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY
668};
669
670var DETECT_STAGGER_CSS_PROPERTIES = {
671 transitionDuration: TRANSITION_DURATION_PROP,
672 transitionDelay: TRANSITION_DELAY_PROP,
673 animationDuration: ANIMATION_DURATION_PROP,
674 animationDelay: ANIMATION_DELAY_PROP
675};
676
677function getCssKeyframeDurationStyle(duration) {
678 return [ANIMATION_DURATION_PROP, duration + 's'];
679}
680
681function getCssDelayStyle(delay, isKeyframeAnimation) {
682 var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP;
683 return [prop, delay + 's'];
684}
685
686function computeCssStyles($window, element, properties) {
687 var styles = Object.create(null);
688 var detectedStyles = $window.getComputedStyle(element) || {};
689 forEach(properties, function(formalStyleName, actualStyleName) {
690 var val = detectedStyles[formalStyleName];
691 if (val) {
692 var c = val.charAt(0);
693
694 // only numerical-based values have a negative sign or digit as the first value
695 if (c === '-' || c === '+' || c >= 0) {
696 val = parseMaxTime(val);
697 }
698
699 // by setting this to null in the event that the delay is not set or is set directly as 0
700 // then we can still allow for zegative values to be used later on and not mistake this
701 // value for being greater than any other negative value.
702 if (val === 0) {
703 val = null;
704 }
705 styles[actualStyleName] = val;
706 }
707 });
708
709 return styles;
710}
711
712function parseMaxTime(str) {
713 var maxValue = 0;
714 var values = str.split(/\s*,\s*/);
715 forEach(values, function(value) {
716 // it's always safe to consider only second values and omit `ms` values since
717 // getComputedStyle will always handle the conversion for us
718 if (value.charAt(value.length - 1) == 's') {
719 value = value.substring(0, value.length - 1);
720 }
721 value = parseFloat(value) || 0;
722 maxValue = maxValue ? Math.max(value, maxValue) : value;
723 });
724 return maxValue;
725}
726
727function truthyTimingValue(val) {
728 return val === 0 || val != null;
729}
730
731function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
732 var style = TRANSITION_PROP;
733 var value = duration + 's';
734 if (applyOnlyDuration) {
735 style += DURATION_KEY;
736 } else {
737 value += ' linear all';
738 }
739 return [style, value];
740}
741
742function createLocalCacheLookup() {
743 var cache = Object.create(null);
744 return {
745 flush: function() {
746 cache = Object.create(null);
747 },
748
749 count: function(key) {
750 var entry = cache[key];
751 return entry ? entry.total : 0;
752 },
753
754 get: function(key) {
755 var entry = cache[key];
756 return entry && entry.value;
757 },
758
759 put: function(key, value) {
760 if (!cache[key]) {
761 cache[key] = { total: 1, value: value };
762 } else {
763 cache[key].total++;
764 }
765 }
766 };
767}
768
769// we do not reassign an already present style value since
770// if we detect the style property value again we may be
771// detecting styles that were added via the `from` styles.
772// We make use of `isDefined` here since an empty string
773// or null value (which is what getPropertyValue will return
774// for a non-existing style) will still be marked as a valid
775// value for the style (a falsy value implies that the style
776// is to be removed at the end of the animation). If we had a simple
777// "OR" statement then it would not be enough to catch that.
778function registerRestorableStyles(backup, node, properties) {
779 forEach(properties, function(prop) {
780 backup[prop] = isDefined(backup[prop])
781 ? backup[prop]
782 : node.style.getPropertyValue(prop);
783 });
784}
785
786var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
787 var gcsLookup = createLocalCacheLookup();
788 var gcsStaggerLookup = createLocalCacheLookup();
789
790 this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout',
791 '$$forceReflow', '$sniffer', '$$rAFScheduler', '$animate',
792 function($window, $$jqLite, $$AnimateRunner, $timeout,
793 $$forceReflow, $sniffer, $$rAFScheduler, $animate) {
794
795 var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
796
797 var parentCounter = 0;
798 function gcsHashFn(node, extraClasses) {
799 var KEY = "$$ngAnimateParentKey";
800 var parentNode = node.parentNode;
801 var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter);
802 return parentID + '-' + node.getAttribute('class') + '-' + extraClasses;
803 }
804
805 function computeCachedCssStyles(node, className, cacheKey, properties) {
806 var timings = gcsLookup.get(cacheKey);
807
808 if (!timings) {
809 timings = computeCssStyles($window, node, properties);
810 if (timings.animationIterationCount === 'infinite') {
811 timings.animationIterationCount = 1;
812 }
813 }
814
815 // we keep putting this in multiple times even though the value and the cacheKey are the same
816 // because we're keeping an interal tally of how many duplicate animations are detected.
817 gcsLookup.put(cacheKey, timings);
818 return timings;
819 }
820
821 function computeCachedCssStaggerStyles(node, className, cacheKey, properties) {
822 var stagger;
823
824 // if we have one or more existing matches of matching elements
825 // containing the same parent + CSS styles (which is how cacheKey works)
826 // then staggering is possible
827 if (gcsLookup.count(cacheKey) > 0) {
828 stagger = gcsStaggerLookup.get(cacheKey);
829
830 if (!stagger) {
831 var staggerClassName = pendClasses(className, '-stagger');
832
833 $$jqLite.addClass(node, staggerClassName);
834
835 stagger = computeCssStyles($window, node, properties);
836
837 // force the conversion of a null value to zero incase not set
838 stagger.animationDuration = Math.max(stagger.animationDuration, 0);
839 stagger.transitionDuration = Math.max(stagger.transitionDuration, 0);
840
841 $$jqLite.removeClass(node, staggerClassName);
842
843 gcsStaggerLookup.put(cacheKey, stagger);
844 }
845 }
846
847 return stagger || {};
848 }
849
850 var cancelLastRAFRequest;
851 var rafWaitQueue = [];
852 function waitUntilQuiet(callback) {
853 rafWaitQueue.push(callback);
854 $$rAFScheduler.waitUntilQuiet(function() {
855 gcsLookup.flush();
856 gcsStaggerLookup.flush();
857
858 // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
859 // PLEASE EXAMINE THE `$$forceReflow` service to understand why.
860 var pageWidth = $$forceReflow();
861
862 // we use a for loop to ensure that if the queue is changed
863 // during this looping then it will consider new requests
864 for (var i = 0; i < rafWaitQueue.length; i++) {
865 rafWaitQueue[i](pageWidth);
866 }
867 rafWaitQueue.length = 0;
868 });
869 }
870
871 function computeTimings(node, className, cacheKey) {
872 var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES);
873 var aD = timings.animationDelay;
874 var tD = timings.transitionDelay;
875 timings.maxDelay = aD && tD
876 ? Math.max(aD, tD)
877 : (aD || tD);
878 timings.maxDuration = Math.max(
879 timings.animationDuration * timings.animationIterationCount,
880 timings.transitionDuration);
881
882 return timings;
883 }
884
885 return function init(element, options) {
886 var restoreStyles = {};
887 var node = getDomNode(element);
888 if (!node
889 || !node.parentNode
890 || !$animate.enabled()) {
891 return closeAndReturnNoopAnimator();
892 }
893
894 options = prepareAnimationOptions(options);
895
896 var temporaryStyles = [];
897 var classes = element.attr('class');
898 var styles = packageStyles(options);
899 var animationClosed;
900 var animationPaused;
901 var animationCompleted;
902 var runner;
903 var runnerHost;
904 var maxDelay;
905 var maxDelayTime;
906 var maxDuration;
907 var maxDurationTime;
908
909 if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) {
910 return closeAndReturnNoopAnimator();
911 }
912
913 var method = options.event && isArray(options.event)
914 ? options.event.join(' ')
915 : options.event;
916
917 var isStructural = method && options.structural;
918 var structuralClassName = '';
919 var addRemoveClassName = '';
920
921 if (isStructural) {
922 structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
923 } else if (method) {
924 structuralClassName = method;
925 }
926
927 if (options.addClass) {
928 addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
929 }
930
931 if (options.removeClass) {
932 if (addRemoveClassName.length) {
933 addRemoveClassName += ' ';
934 }
935 addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX);
936 }
937
938 // there may be a situation where a structural animation is combined together
939 // with CSS classes that need to resolve before the animation is computed.
940 // However this means that there is no explicit CSS code to block the animation
941 // from happening (by setting 0s none in the class name). If this is the case
942 // we need to apply the classes before the first rAF so we know to continue if
943 // there actually is a detected transition or keyframe animation
944 if (options.applyClassesEarly && addRemoveClassName.length) {
945 applyAnimationClasses(element, options);
946 }
947
948 var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim();
949 var fullClassName = classes + ' ' + preparationClasses;
950 var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
951 var hasToStyles = styles.to && Object.keys(styles.to).length > 0;
952 var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0;
953
954 // there is no way we can trigger an animation if no styles and
955 // no classes are being applied which would then trigger a transition,
956 // unless there a is raw keyframe value that is applied to the element.
957 if (!containsKeyframeAnimation
958 && !hasToStyles
959 && !preparationClasses) {
960 return closeAndReturnNoopAnimator();
961 }
962
963 var cacheKey, stagger;
964 if (options.stagger > 0) {
965 var staggerVal = parseFloat(options.stagger);
966 stagger = {
967 transitionDelay: staggerVal,
968 animationDelay: staggerVal,
969 transitionDuration: 0,
970 animationDuration: 0
971 };
972 } else {
973 cacheKey = gcsHashFn(node, fullClassName);
974 stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES);
975 }
976
977 if (!options.$$skipPreparationClasses) {
978 $$jqLite.addClass(element, preparationClasses);
979 }
980
981 var applyOnlyDuration;
982
983 if (options.transitionStyle) {
984 var transitionStyle = [TRANSITION_PROP, options.transitionStyle];
985 applyInlineStyle(node, transitionStyle);
986 temporaryStyles.push(transitionStyle);
987 }
988
989 if (options.duration >= 0) {
990 applyOnlyDuration = node.style[TRANSITION_PROP].length > 0;
991 var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration);
992
993 // we set the duration so that it will be picked up by getComputedStyle later
994 applyInlineStyle(node, durationStyle);
995 temporaryStyles.push(durationStyle);
996 }
997
998 if (options.keyframeStyle) {
999 var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle];
1000 applyInlineStyle(node, keyframeStyle);
1001 temporaryStyles.push(keyframeStyle);
1002 }
1003
1004 var itemIndex = stagger
1005 ? options.staggerIndex >= 0
1006 ? options.staggerIndex
1007 : gcsLookup.count(cacheKey)
1008 : 0;
1009
1010 var isFirst = itemIndex === 0;
1011
1012 // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY
1013 // without causing any combination of transitions to kick in. By adding a negative delay value
1014 // it forces the setup class' transition to end immediately. We later then remove the negative
1015 // transition delay to allow for the transition to naturally do it's thing. The beauty here is
1016 // that if there is no transition defined then nothing will happen and this will also allow
1017 // other transitions to be stacked on top of each other without any chopping them out.
1018 if (isFirst && !options.skipBlocking) {
1019 blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
1020 }
1021
1022 var timings = computeTimings(node, fullClassName, cacheKey);
1023 var relativeDelay = timings.maxDelay;
1024 maxDelay = Math.max(relativeDelay, 0);
1025 maxDuration = timings.maxDuration;
1026
1027 var flags = {};
1028 flags.hasTransitions = timings.transitionDuration > 0;
1029 flags.hasAnimations = timings.animationDuration > 0;
1030 flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all';
1031 flags.applyTransitionDuration = hasToStyles && (
1032 (flags.hasTransitions && !flags.hasTransitionAll)
1033 || (flags.hasAnimations && !flags.hasTransitions));
1034 flags.applyAnimationDuration = options.duration && flags.hasAnimations;
1035 flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions);
1036 flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations;
1037 flags.recalculateTimingStyles = addRemoveClassName.length > 0;
1038
1039 if (flags.applyTransitionDuration || flags.applyAnimationDuration) {
1040 maxDuration = options.duration ? parseFloat(options.duration) : maxDuration;
1041
1042 if (flags.applyTransitionDuration) {
1043 flags.hasTransitions = true;
1044 timings.transitionDuration = maxDuration;
1045 applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0;
1046 temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration));
1047 }
1048
1049 if (flags.applyAnimationDuration) {
1050 flags.hasAnimations = true;
1051 timings.animationDuration = maxDuration;
1052 temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration));
1053 }
1054 }
1055
1056 if (maxDuration === 0 && !flags.recalculateTimingStyles) {
1057 return closeAndReturnNoopAnimator();
1058 }
1059
1060 if (options.delay != null) {
1061 var delayStyle = parseFloat(options.delay);
1062
1063 if (flags.applyTransitionDelay) {
1064 temporaryStyles.push(getCssDelayStyle(delayStyle));
1065 }
1066
1067 if (flags.applyAnimationDelay) {
1068 temporaryStyles.push(getCssDelayStyle(delayStyle, true));
1069 }
1070 }
1071
1072 // we need to recalculate the delay value since we used a pre-emptive negative
1073 // delay value and the delay value is required for the final event checking. This
1074 // property will ensure that this will happen after the RAF phase has passed.
1075 if (options.duration == null && timings.transitionDuration > 0) {
1076 flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst;
1077 }
1078
1079 maxDelayTime = maxDelay * ONE_SECOND;
1080 maxDurationTime = maxDuration * ONE_SECOND;
1081 if (!options.skipBlocking) {
1082 flags.blockTransition = timings.transitionDuration > 0;
1083 flags.blockKeyframeAnimation = timings.animationDuration > 0 &&
1084 stagger.animationDelay > 0 &&
1085 stagger.animationDuration === 0;
1086 }
1087
1088 if (options.from) {
1089 if (options.cleanupStyles) {
1090 registerRestorableStyles(restoreStyles, node, Object.keys(options.from));
1091 }
1092 applyAnimationFromStyles(element, options);
1093 }
1094
1095 if (flags.blockTransition || flags.blockKeyframeAnimation) {
1096 applyBlocking(maxDuration);
1097 } else if (!options.skipBlocking) {
1098 blockTransitions(node, false);
1099 }
1100
1101 // TODO(matsko): for 1.5 change this code to have an animator object for better debugging
1102 return {
1103 $$willAnimate: true,
1104 end: endFn,
1105 start: function() {
1106 if (animationClosed) return;
1107
1108 runnerHost = {
1109 end: endFn,
1110 cancel: cancelFn,
1111 resume: null, //this will be set during the start() phase
1112 pause: null
1113 };
1114
1115 runner = new $$AnimateRunner(runnerHost);
1116
1117 waitUntilQuiet(start);
1118
1119 // we don't have access to pause/resume the animation
1120 // since it hasn't run yet. AnimateRunner will therefore
1121 // set noop functions for resume and pause and they will
1122 // later be overridden once the animation is triggered
1123 return runner;
1124 }
1125 };
1126
1127 function endFn() {
1128 close();
1129 }
1130
1131 function cancelFn() {
1132 close(true);
1133 }
1134
1135 function close(rejected) { // jshint ignore:line
1136 // if the promise has been called already then we shouldn't close
1137 // the animation again
1138 if (animationClosed || (animationCompleted && animationPaused)) return;
1139 animationClosed = true;
1140 animationPaused = false;
1141
1142 if (!options.$$skipPreparationClasses) {
1143 $$jqLite.removeClass(element, preparationClasses);
1144 }
1145 $$jqLite.removeClass(element, activeClasses);
1146
1147 blockKeyframeAnimations(node, false);
1148 blockTransitions(node, false);
1149
1150 forEach(temporaryStyles, function(entry) {
1151 // There is only one way to remove inline style properties entirely from elements.
1152 // By using `removeProperty` this works, but we need to convert camel-cased CSS
1153 // styles down to hyphenated values.
1154 node.style[entry[0]] = '';
1155 });
1156
1157 applyAnimationClasses(element, options);
1158 applyAnimationStyles(element, options);
1159
1160 if (Object.keys(restoreStyles).length) {
1161 forEach(restoreStyles, function(value, prop) {
1162 value ? node.style.setProperty(prop, value)
1163 : node.style.removeProperty(prop);
1164 });
1165 }
1166
1167 // the reason why we have this option is to allow a synchronous closing callback
1168 // that is fired as SOON as the animation ends (when the CSS is removed) or if
1169 // the animation never takes off at all. A good example is a leave animation since
1170 // the element must be removed just after the animation is over or else the element
1171 // will appear on screen for one animation frame causing an overbearing flicker.
1172 if (options.onDone) {
1173 options.onDone();
1174 }
1175
1176 // if the preparation function fails then the promise is not setup
1177 if (runner) {
1178 runner.complete(!rejected);
1179 }
1180 }
1181
1182 function applyBlocking(duration) {
1183 if (flags.blockTransition) {
1184 blockTransitions(node, duration);
1185 }
1186
1187 if (flags.blockKeyframeAnimation) {
1188 blockKeyframeAnimations(node, !!duration);
1189 }
1190 }
1191
1192 function closeAndReturnNoopAnimator() {
1193 runner = new $$AnimateRunner({
1194 end: endFn,
1195 cancel: cancelFn
1196 });
1197
1198 // should flush the cache animation
1199 waitUntilQuiet(noop);
1200 close();
1201
1202 return {
1203 $$willAnimate: false,
1204 start: function() {
1205 return runner;
1206 },
1207 end: endFn
1208 };
1209 }
1210
1211 function start() {
1212 if (animationClosed) return;
1213 if (!node.parentNode) {
1214 close();
1215 return;
1216 }
1217
1218 var startTime, events = [];
1219
1220 // even though we only pause keyframe animations here the pause flag
1221 // will still happen when transitions are used. Only the transition will
1222 // not be paused since that is not possible. If the animation ends when
1223 // paused then it will not complete until unpaused or cancelled.
1224 var playPause = function(playAnimation) {
1225 if (!animationCompleted) {
1226 animationPaused = !playAnimation;
1227 if (timings.animationDuration) {
1228 var value = blockKeyframeAnimations(node, animationPaused);
1229 animationPaused
1230 ? temporaryStyles.push(value)
1231 : removeFromArray(temporaryStyles, value);
1232 }
1233 } else if (animationPaused && playAnimation) {
1234 animationPaused = false;
1235 close();
1236 }
1237 };
1238
1239 // checking the stagger duration prevents an accidently cascade of the CSS delay style
1240 // being inherited from the parent. If the transition duration is zero then we can safely
1241 // rely that the delay value is an intential stagger delay style.
1242 var maxStagger = itemIndex > 0
1243 && ((timings.transitionDuration && stagger.transitionDuration === 0) ||
1244 (timings.animationDuration && stagger.animationDuration === 0))
1245 && Math.max(stagger.animationDelay, stagger.transitionDelay);
1246 if (maxStagger) {
1247 $timeout(triggerAnimationStart,
1248 Math.floor(maxStagger * itemIndex * ONE_SECOND),
1249 false);
1250 } else {
1251 triggerAnimationStart();
1252 }
1253
1254 // this will decorate the existing promise runner with pause/resume methods
1255 runnerHost.resume = function() {
1256 playPause(true);
1257 };
1258
1259 runnerHost.pause = function() {
1260 playPause(false);
1261 };
1262
1263 function triggerAnimationStart() {
1264 // just incase a stagger animation kicks in when the animation
1265 // itself was cancelled entirely
1266 if (animationClosed) return;
1267
1268 applyBlocking(false);
1269
1270 forEach(temporaryStyles, function(entry) {
1271 var key = entry[0];
1272 var value = entry[1];
1273 node.style[key] = value;
1274 });
1275
1276 applyAnimationClasses(element, options);
1277 $$jqLite.addClass(element, activeClasses);
1278
1279 if (flags.recalculateTimingStyles) {
1280 fullClassName = node.className + ' ' + preparationClasses;
1281 cacheKey = gcsHashFn(node, fullClassName);
1282
1283 timings = computeTimings(node, fullClassName, cacheKey);
1284 relativeDelay = timings.maxDelay;
1285 maxDelay = Math.max(relativeDelay, 0);
1286 maxDuration = timings.maxDuration;
1287
1288 if (maxDuration === 0) {
1289 close();
1290 return;
1291 }
1292
1293 flags.hasTransitions = timings.transitionDuration > 0;
1294 flags.hasAnimations = timings.animationDuration > 0;
1295 }
1296
1297 if (flags.applyAnimationDelay) {
1298 relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay)
1299 ? parseFloat(options.delay)
1300 : relativeDelay;
1301
1302 maxDelay = Math.max(relativeDelay, 0);
1303 timings.animationDelay = relativeDelay;
1304 delayStyle = getCssDelayStyle(relativeDelay, true);
1305 temporaryStyles.push(delayStyle);
1306 node.style[delayStyle[0]] = delayStyle[1];
1307 }
1308
1309 maxDelayTime = maxDelay * ONE_SECOND;
1310 maxDurationTime = maxDuration * ONE_SECOND;
1311
1312 if (options.easing) {
1313 var easeProp, easeVal = options.easing;
1314 if (flags.hasTransitions) {
1315 easeProp = TRANSITION_PROP + TIMING_KEY;
1316 temporaryStyles.push([easeProp, easeVal]);
1317 node.style[easeProp] = easeVal;
1318 }
1319 if (flags.hasAnimations) {
1320 easeProp = ANIMATION_PROP + TIMING_KEY;
1321 temporaryStyles.push([easeProp, easeVal]);
1322 node.style[easeProp] = easeVal;
1323 }
1324 }
1325
1326 if (timings.transitionDuration) {
1327 events.push(TRANSITIONEND_EVENT);
1328 }
1329
1330 if (timings.animationDuration) {
1331 events.push(ANIMATIONEND_EVENT);
1332 }
1333
1334 startTime = Date.now();
1335 var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime;
1336 var endTime = startTime + timerTime;
1337
1338 var animationsData = element.data(ANIMATE_TIMER_KEY) || [];
1339 var setupFallbackTimer = true;
1340 if (animationsData.length) {
1341 var currentTimerData = animationsData[0];
1342 setupFallbackTimer = endTime > currentTimerData.expectedEndTime;
1343 if (setupFallbackTimer) {
1344 $timeout.cancel(currentTimerData.timer);
1345 } else {
1346 animationsData.push(close);
1347 }
1348 }
1349
1350 if (setupFallbackTimer) {
1351 var timer = $timeout(onAnimationExpired, timerTime, false);
1352 animationsData[0] = {
1353 timer: timer,
1354 expectedEndTime: endTime
1355 };
1356 animationsData.push(close);
1357 element.data(ANIMATE_TIMER_KEY, animationsData);
1358 }
1359
1360 element.on(events.join(' '), onAnimationProgress);
1361 if (options.to) {
1362 if (options.cleanupStyles) {
1363 registerRestorableStyles(restoreStyles, node, Object.keys(options.to));
1364 }
1365 applyAnimationToStyles(element, options);
1366 }
1367 }
1368
1369 function onAnimationExpired() {
1370 var animationsData = element.data(ANIMATE_TIMER_KEY);
1371
1372 // this will be false in the event that the element was
1373 // removed from the DOM (via a leave animation or something
1374 // similar)
1375 if (animationsData) {
1376 for (var i = 1; i < animationsData.length; i++) {
1377 animationsData[i]();
1378 }
1379 element.removeData(ANIMATE_TIMER_KEY);
1380 }
1381 }
1382
1383 function onAnimationProgress(event) {
1384 event.stopPropagation();
1385 var ev = event.originalEvent || event;
1386 var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now();
1387
1388 /* Firefox (or possibly just Gecko) likes to not round values up
1389 * when a ms measurement is used for the animation */
1390 var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES));
1391
1392 /* $manualTimeStamp is a mocked timeStamp value which is set
1393 * within browserTrigger(). This is only here so that tests can
1394 * mock animations properly. Real events fallback to event.timeStamp,
1395 * or, if they don't, then a timeStamp is automatically created for them.
1396 * We're checking to see if the timeStamp surpasses the expected delay,
1397 * but we're using elapsedTime instead of the timeStamp on the 2nd
1398 * pre-condition since animations sometimes close off early */
1399 if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
1400 // we set this flag to ensure that if the transition is paused then, when resumed,
1401 // the animation will automatically close itself since transitions cannot be paused.
1402 animationCompleted = true;
1403 close();
1404 }
1405 }
1406 }
1407 };
1408 }];
1409}];
1410
1411var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) {
1412 $$animationProvider.drivers.push('$$animateCssDriver');
1413
1414 var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
1415 var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor';
1416
1417 var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
1418 var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
1419
1420 function isDocumentFragment(node) {
1421 return node.parentNode && node.parentNode.nodeType === 11;
1422 }
1423
1424 this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document',
1425 function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) {
1426
1427 // only browsers that support these properties can render animations
1428 if (!$sniffer.animations && !$sniffer.transitions) return noop;
1429
1430 var bodyNode = $document[0].body;
1431 var rootNode = getDomNode($rootElement);
1432
1433 var rootBodyElement = jqLite(
1434 // this is to avoid using something that exists outside of the body
1435 // we also special case the doc fragement case because our unit test code
1436 // appends the $rootElement to the body after the app has been bootstrapped
1437 isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode
1438 );
1439
1440 var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
1441
1442 return function initDriverFn(animationDetails) {
1443 return animationDetails.from && animationDetails.to
1444 ? prepareFromToAnchorAnimation(animationDetails.from,
1445 animationDetails.to,
1446 animationDetails.classes,
1447 animationDetails.anchors)
1448 : prepareRegularAnimation(animationDetails);
1449 };
1450
1451 function filterCssClasses(classes) {
1452 //remove all the `ng-` stuff
1453 return classes.replace(/\bng-\S+\b/g, '');
1454 }
1455
1456 function getUniqueValues(a, b) {
1457 if (isString(a)) a = a.split(' ');
1458 if (isString(b)) b = b.split(' ');
1459 return a.filter(function(val) {
1460 return b.indexOf(val) === -1;
1461 }).join(' ');
1462 }
1463
1464 function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
1465 var clone = jqLite(getDomNode(outAnchor).cloneNode(true));
1466 var startingClasses = filterCssClasses(getClassVal(clone));
1467
1468 outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
1469 inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
1470
1471 clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME);
1472
1473 rootBodyElement.append(clone);
1474
1475 var animatorIn, animatorOut = prepareOutAnimation();
1476
1477 // the user may not end up using the `out` animation and
1478 // only making use of the `in` animation or vice-versa.
1479 // In either case we should allow this and not assume the
1480 // animation is over unless both animations are not used.
1481 if (!animatorOut) {
1482 animatorIn = prepareInAnimation();
1483 if (!animatorIn) {
1484 return end();
1485 }
1486 }
1487
1488 var startingAnimator = animatorOut || animatorIn;
1489
1490 return {
1491 start: function() {
1492 var runner;
1493
1494 var currentAnimation = startingAnimator.start();
1495 currentAnimation.done(function() {
1496 currentAnimation = null;
1497 if (!animatorIn) {
1498 animatorIn = prepareInAnimation();
1499 if (animatorIn) {
1500 currentAnimation = animatorIn.start();
1501 currentAnimation.done(function() {
1502 currentAnimation = null;
1503 end();
1504 runner.complete();
1505 });
1506 return currentAnimation;
1507 }
1508 }
1509 // in the event that there is no `in` animation
1510 end();
1511 runner.complete();
1512 });
1513
1514 runner = new $$AnimateRunner({
1515 end: endFn,
1516 cancel: endFn
1517 });
1518
1519 return runner;
1520
1521 function endFn() {
1522 if (currentAnimation) {
1523 currentAnimation.end();
1524 }
1525 }
1526 }
1527 };
1528
1529 function calculateAnchorStyles(anchor) {
1530 var styles = {};
1531
1532 var coords = getDomNode(anchor).getBoundingClientRect();
1533
1534 // we iterate directly since safari messes up and doesn't return
1535 // all the keys for the coods object when iterated
1536 forEach(['width','height','top','left'], function(key) {
1537 var value = coords[key];
1538 switch (key) {
1539 case 'top':
1540 value += bodyNode.scrollTop;
1541 break;
1542 case 'left':
1543 value += bodyNode.scrollLeft;
1544 break;
1545 }
1546 styles[key] = Math.floor(value) + 'px';
1547 });
1548 return styles;
1549 }
1550
1551 function prepareOutAnimation() {
1552 var animator = $animateCss(clone, {
1553 addClass: NG_OUT_ANCHOR_CLASS_NAME,
1554 delay: true,
1555 from: calculateAnchorStyles(outAnchor)
1556 });
1557
1558 // read the comment within `prepareRegularAnimation` to understand
1559 // why this check is necessary
1560 return animator.$$willAnimate ? animator : null;
1561 }
1562
1563 function getClassVal(element) {
1564 return element.attr('class') || '';
1565 }
1566
1567 function prepareInAnimation() {
1568 var endingClasses = filterCssClasses(getClassVal(inAnchor));
1569 var toAdd = getUniqueValues(endingClasses, startingClasses);
1570 var toRemove = getUniqueValues(startingClasses, endingClasses);
1571
1572 var animator = $animateCss(clone, {
1573 to: calculateAnchorStyles(inAnchor),
1574 addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd,
1575 removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove,
1576 delay: true
1577 });
1578
1579 // read the comment within `prepareRegularAnimation` to understand
1580 // why this check is necessary
1581 return animator.$$willAnimate ? animator : null;
1582 }
1583
1584 function end() {
1585 clone.remove();
1586 outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
1587 inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME);
1588 }
1589 }
1590
1591 function prepareFromToAnchorAnimation(from, to, classes, anchors) {
1592 var fromAnimation = prepareRegularAnimation(from, noop);
1593 var toAnimation = prepareRegularAnimation(to, noop);
1594
1595 var anchorAnimations = [];
1596 forEach(anchors, function(anchor) {
1597 var outElement = anchor['out'];
1598 var inElement = anchor['in'];
1599 var animator = prepareAnchoredAnimation(classes, outElement, inElement);
1600 if (animator) {
1601 anchorAnimations.push(animator);
1602 }
1603 });
1604
1605 // no point in doing anything when there are no elements to animate
1606 if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return;
1607
1608 return {
1609 start: function() {
1610 var animationRunners = [];
1611
1612 if (fromAnimation) {
1613 animationRunners.push(fromAnimation.start());
1614 }
1615
1616 if (toAnimation) {
1617 animationRunners.push(toAnimation.start());
1618 }
1619
1620 forEach(anchorAnimations, function(animation) {
1621 animationRunners.push(animation.start());
1622 });
1623
1624 var runner = new $$AnimateRunner({
1625 end: endFn,
1626 cancel: endFn // CSS-driven animations cannot be cancelled, only ended
1627 });
1628
1629 $$AnimateRunner.all(animationRunners, function(status) {
1630 runner.complete(status);
1631 });
1632
1633 return runner;
1634
1635 function endFn() {
1636 forEach(animationRunners, function(runner) {
1637 runner.end();
1638 });
1639 }
1640 }
1641 };
1642 }
1643
1644 function prepareRegularAnimation(animationDetails) {
1645 var element = animationDetails.element;
1646 var options = animationDetails.options || {};
1647
1648 if (animationDetails.structural) {
1649 options.event = animationDetails.event;
1650 options.structural = true;
1651 options.applyClassesEarly = true;
1652
1653 // we special case the leave animation since we want to ensure that
1654 // the element is removed as soon as the animation is over. Otherwise
1655 // a flicker might appear or the element may not be removed at all
1656 if (animationDetails.event === 'leave') {
1657 options.onDone = options.domOperation;
1658 }
1659 }
1660
1661 // We assign the preparationClasses as the actual animation event since
1662 // the internals of $animateCss will just suffix the event token values
1663 // with `-active` to trigger the animation.
1664 if (options.preparationClasses) {
1665 options.event = concatWithSpace(options.event, options.preparationClasses);
1666 }
1667
1668 var animator = $animateCss(element, options);
1669
1670 // the driver lookup code inside of $$animation attempts to spawn a
1671 // driver one by one until a driver returns a.$$willAnimate animator object.
1672 // $animateCss will always return an object, however, it will pass in
1673 // a flag as a hint as to whether an animation was detected or not
1674 return animator.$$willAnimate ? animator : null;
1675 }
1676 }];
1677}];
1678
1679// TODO(matsko): use caching here to speed things up for detection
1680// TODO(matsko): add documentation
1681// by the time...
1682
1683var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
1684 this.$get = ['$injector', '$$AnimateRunner', '$$jqLite',
1685 function($injector, $$AnimateRunner, $$jqLite) {
1686
1687 var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
1688 // $animateJs(element, 'enter');
1689 return function(element, event, classes, options) {
1690 // the `classes` argument is optional and if it is not used
1691 // then the classes will be resolved from the element's className
1692 // property as well as options.addClass/options.removeClass.
1693 if (arguments.length === 3 && isObject(classes)) {
1694 options = classes;
1695 classes = null;
1696 }
1697
1698 options = prepareAnimationOptions(options);
1699 if (!classes) {
1700 classes = element.attr('class') || '';
1701 if (options.addClass) {
1702 classes += ' ' + options.addClass;
1703 }
1704 if (options.removeClass) {
1705 classes += ' ' + options.removeClass;
1706 }
1707 }
1708
1709 var classesToAdd = options.addClass;
1710 var classesToRemove = options.removeClass;
1711
1712 // the lookupAnimations function returns a series of animation objects that are
1713 // matched up with one or more of the CSS classes. These animation objects are
1714 // defined via the module.animation factory function. If nothing is detected then
1715 // we don't return anything which then makes $animation query the next driver.
1716 var animations = lookupAnimations(classes);
1717 var before, after;
1718 if (animations.length) {
1719 var afterFn, beforeFn;
1720 if (event == 'leave') {
1721 beforeFn = 'leave';
1722 afterFn = 'afterLeave'; // TODO(matsko): get rid of this
1723 } else {
1724 beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
1725 afterFn = event;
1726 }
1727
1728 if (event !== 'enter' && event !== 'move') {
1729 before = packageAnimations(element, event, options, animations, beforeFn);
1730 }
1731 after = packageAnimations(element, event, options, animations, afterFn);
1732 }
1733
1734 // no matching animations
1735 if (!before && !after) return;
1736
1737 function applyOptions() {
1738 options.domOperation();
1739 applyAnimationClasses(element, options);
1740 }
1741
1742 return {
1743 start: function() {
1744 var closeActiveAnimations;
1745 var chain = [];
1746
1747 if (before) {
1748 chain.push(function(fn) {
1749 closeActiveAnimations = before(fn);
1750 });
1751 }
1752
1753 if (chain.length) {
1754 chain.push(function(fn) {
1755 applyOptions();
1756 fn(true);
1757 });
1758 } else {
1759 applyOptions();
1760 }
1761
1762 if (after) {
1763 chain.push(function(fn) {
1764 closeActiveAnimations = after(fn);
1765 });
1766 }
1767
1768 var animationClosed = false;
1769 var runner = new $$AnimateRunner({
1770 end: function() {
1771 endAnimations();
1772 },
1773 cancel: function() {
1774 endAnimations(true);
1775 }
1776 });
1777
1778 $$AnimateRunner.chain(chain, onComplete);
1779 return runner;
1780
1781 function onComplete(success) {
1782 animationClosed = true;
1783 applyOptions();
1784 applyAnimationStyles(element, options);
1785 runner.complete(success);
1786 }
1787
1788 function endAnimations(cancelled) {
1789 if (!animationClosed) {
1790 (closeActiveAnimations || noop)(cancelled);
1791 onComplete(cancelled);
1792 }
1793 }
1794 }
1795 };
1796
1797 function executeAnimationFn(fn, element, event, options, onDone) {
1798 var args;
1799 switch (event) {
1800 case 'animate':
1801 args = [element, options.from, options.to, onDone];
1802 break;
1803
1804 case 'setClass':
1805 args = [element, classesToAdd, classesToRemove, onDone];
1806 break;
1807
1808 case 'addClass':
1809 args = [element, classesToAdd, onDone];
1810 break;
1811
1812 case 'removeClass':
1813 args = [element, classesToRemove, onDone];
1814 break;
1815
1816 default:
1817 args = [element, onDone];
1818 break;
1819 }
1820
1821 args.push(options);
1822
1823 var value = fn.apply(fn, args);
1824 if (value) {
1825 if (isFunction(value.start)) {
1826 value = value.start();
1827 }
1828
1829 if (value instanceof $$AnimateRunner) {
1830 value.done(onDone);
1831 } else if (isFunction(value)) {
1832 // optional onEnd / onCancel callback
1833 return value;
1834 }
1835 }
1836
1837 return noop;
1838 }
1839
1840 function groupEventedAnimations(element, event, options, animations, fnName) {
1841 var operations = [];
1842 forEach(animations, function(ani) {
1843 var animation = ani[fnName];
1844 if (!animation) return;
1845
1846 // note that all of these animations will run in parallel
1847 operations.push(function() {
1848 var runner;
1849 var endProgressCb;
1850
1851 var resolved = false;
1852 var onAnimationComplete = function(rejected) {
1853 if (!resolved) {
1854 resolved = true;
1855 (endProgressCb || noop)(rejected);
1856 runner.complete(!rejected);
1857 }
1858 };
1859
1860 runner = new $$AnimateRunner({
1861 end: function() {
1862 onAnimationComplete();
1863 },
1864 cancel: function() {
1865 onAnimationComplete(true);
1866 }
1867 });
1868
1869 endProgressCb = executeAnimationFn(animation, element, event, options, function(result) {
1870 var cancelled = result === false;
1871 onAnimationComplete(cancelled);
1872 });
1873
1874 return runner;
1875 });
1876 });
1877
1878 return operations;
1879 }
1880
1881 function packageAnimations(element, event, options, animations, fnName) {
1882 var operations = groupEventedAnimations(element, event, options, animations, fnName);
1883 if (operations.length === 0) {
1884 var a,b;
1885 if (fnName === 'beforeSetClass') {
1886 a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass');
1887 b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass');
1888 } else if (fnName === 'setClass') {
1889 a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass');
1890 b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass');
1891 }
1892
1893 if (a) {
1894 operations = operations.concat(a);
1895 }
1896 if (b) {
1897 operations = operations.concat(b);
1898 }
1899 }
1900
1901 if (operations.length === 0) return;
1902
1903 // TODO(matsko): add documentation
1904 return function startAnimation(callback) {
1905 var runners = [];
1906 if (operations.length) {
1907 forEach(operations, function(animateFn) {
1908 runners.push(animateFn());
1909 });
1910 }
1911
1912 runners.length ? $$AnimateRunner.all(runners, callback) : callback();
1913
1914 return function endFn(reject) {
1915 forEach(runners, function(runner) {
1916 reject ? runner.cancel() : runner.end();
1917 });
1918 };
1919 };
1920 }
1921 };
1922
1923 function lookupAnimations(classes) {
1924 classes = isArray(classes) ? classes : classes.split(' ');
1925 var matches = [], flagMap = {};
1926 for (var i=0; i < classes.length; i++) {
1927 var klass = classes[i],
1928 animationFactory = $animateProvider.$$registeredAnimations[klass];
1929 if (animationFactory && !flagMap[klass]) {
1930 matches.push($injector.get(animationFactory));
1931 flagMap[klass] = true;
1932 }
1933 }
1934 return matches;
1935 }
1936 }];
1937}];
1938
1939var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) {
1940 $$animationProvider.drivers.push('$$animateJsDriver');
1941 this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) {
1942 return function initDriverFn(animationDetails) {
1943 if (animationDetails.from && animationDetails.to) {
1944 var fromAnimation = prepareAnimation(animationDetails.from);
1945 var toAnimation = prepareAnimation(animationDetails.to);
1946 if (!fromAnimation && !toAnimation) return;
1947
1948 return {
1949 start: function() {
1950 var animationRunners = [];
1951
1952 if (fromAnimation) {
1953 animationRunners.push(fromAnimation.start());
1954 }
1955
1956 if (toAnimation) {
1957 animationRunners.push(toAnimation.start());
1958 }
1959
1960 $$AnimateRunner.all(animationRunners, done);
1961
1962 var runner = new $$AnimateRunner({
1963 end: endFnFactory(),
1964 cancel: endFnFactory()
1965 });
1966
1967 return runner;
1968
1969 function endFnFactory() {
1970 return function() {
1971 forEach(animationRunners, function(runner) {
1972 // at this point we cannot cancel animations for groups just yet. 1.5+
1973 runner.end();
1974 });
1975 };
1976 }
1977
1978 function done(status) {
1979 runner.complete(status);
1980 }
1981 }
1982 };
1983 } else {
1984 return prepareAnimation(animationDetails);
1985 }
1986 };
1987
1988 function prepareAnimation(animationDetails) {
1989 // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations
1990 var element = animationDetails.element;
1991 var event = animationDetails.event;
1992 var options = animationDetails.options;
1993 var classes = animationDetails.classes;
1994 return $$animateJs(element, event, classes, options);
1995 }
1996 }];
1997}];
1998
1999var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
2000var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
2001var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
2002 var PRE_DIGEST_STATE = 1;
2003 var RUNNING_STATE = 2;
2004
2005 var rules = this.rules = {
2006 skip: [],
2007 cancel: [],
2008 join: []
2009 };
2010
2011 function isAllowed(ruleType, element, currentAnimation, previousAnimation) {
2012 return rules[ruleType].some(function(fn) {
2013 return fn(element, currentAnimation, previousAnimation);
2014 });
2015 }
2016
2017 function hasAnimationClasses(options, and) {
2018 options = options || {};
2019 var a = (options.addClass || '').length > 0;
2020 var b = (options.removeClass || '').length > 0;
2021 return and ? a && b : a || b;
2022 }
2023
2024 rules.join.push(function(element, newAnimation, currentAnimation) {
2025 // if the new animation is class-based then we can just tack that on
2026 return !newAnimation.structural && hasAnimationClasses(newAnimation.options);
2027 });
2028
2029 rules.skip.push(function(element, newAnimation, currentAnimation) {
2030 // there is no need to animate anything if no classes are being added and
2031 // there is no structural animation that will be triggered
2032 return !newAnimation.structural && !hasAnimationClasses(newAnimation.options);
2033 });
2034
2035 rules.skip.push(function(element, newAnimation, currentAnimation) {
2036 // why should we trigger a new structural animation if the element will
2037 // be removed from the DOM anyway?
2038 return currentAnimation.event == 'leave' && newAnimation.structural;
2039 });
2040
2041 rules.skip.push(function(element, newAnimation, currentAnimation) {
2042 // if there is an ongoing current animation then don't even bother running the class-based animation
2043 return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural;
2044 });
2045
2046 rules.cancel.push(function(element, newAnimation, currentAnimation) {
2047 // there can never be two structural animations running at the same time
2048 return currentAnimation.structural && newAnimation.structural;
2049 });
2050
2051 rules.cancel.push(function(element, newAnimation, currentAnimation) {
2052 // if the previous animation is already running, but the new animation will
2053 // be triggered, but the new animation is structural
2054 return currentAnimation.state === RUNNING_STATE && newAnimation.structural;
2055 });
2056
2057 rules.cancel.push(function(element, newAnimation, currentAnimation) {
2058 var nO = newAnimation.options;
2059 var cO = currentAnimation.options;
2060
2061 // if the exact same CSS class is added/removed then it's safe to cancel it
2062 return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass);
2063 });
2064
2065 this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
2066 '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
2067 function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
2068 $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
2069
2070 var activeAnimationsLookup = new $$HashMap();
2071 var disabledElementsLookup = new $$HashMap();
2072 var animationsEnabled = null;
2073
2074 function postDigestTaskFactory() {
2075 var postDigestCalled = false;
2076 return function(fn) {
2077 // we only issue a call to postDigest before
2078 // it has first passed. This prevents any callbacks
2079 // from not firing once the animation has completed
2080 // since it will be out of the digest cycle.
2081 if (postDigestCalled) {
2082 fn();
2083 } else {
2084 $rootScope.$$postDigest(function() {
2085 postDigestCalled = true;
2086 fn();
2087 });
2088 }
2089 };
2090 }
2091
2092 // Wait until all directive and route-related templates are downloaded and
2093 // compiled. The $templateRequest.totalPendingRequests variable keeps track of
2094 // all of the remote templates being currently downloaded. If there are no
2095 // templates currently downloading then the watcher will still fire anyway.
2096 var deregisterWatch = $rootScope.$watch(
2097 function() { return $templateRequest.totalPendingRequests === 0; },
2098 function(isEmpty) {
2099 if (!isEmpty) return;
2100 deregisterWatch();
2101
2102 // Now that all templates have been downloaded, $animate will wait until
2103 // the post digest queue is empty before enabling animations. By having two
2104 // calls to $postDigest calls we can ensure that the flag is enabled at the
2105 // very end of the post digest queue. Since all of the animations in $animate
2106 // use $postDigest, it's important that the code below executes at the end.
2107 // This basically means that the page is fully downloaded and compiled before
2108 // any animations are triggered.
2109 $rootScope.$$postDigest(function() {
2110 $rootScope.$$postDigest(function() {
2111 // we check for null directly in the event that the application already called
2112 // .enabled() with whatever arguments that it provided it with
2113 if (animationsEnabled === null) {
2114 animationsEnabled = true;
2115 }
2116 });
2117 });
2118 }
2119 );
2120
2121 var callbackRegistry = {};
2122
2123 // remember that the classNameFilter is set during the provider/config
2124 // stage therefore we can optimize here and setup a helper function
2125 var classNameFilter = $animateProvider.classNameFilter();
2126 var isAnimatableClassName = !classNameFilter
2127 ? function() { return true; }
2128 : function(className) {
2129 return classNameFilter.test(className);
2130 };
2131
2132 var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
2133
2134 function normalizeAnimationOptions(element, options) {
2135 return mergeAnimationOptions(element, options, {});
2136 }
2137
2138 function findCallbacks(parent, element, event) {
2139 var targetNode = getDomNode(element);
2140 var targetParentNode = getDomNode(parent);
2141
2142 var matches = [];
2143 var entries = callbackRegistry[event];
2144 if (entries) {
2145 forEach(entries, function(entry) {
2146 if (entry.node.contains(targetNode)) {
2147 matches.push(entry.callback);
2148 } else if (event === 'leave' && entry.node.contains(targetParentNode)) {
2149 matches.push(entry.callback);
2150 }
2151 });
2152 }
2153
2154 return matches;
2155 }
2156
2157 return {
2158 on: function(event, container, callback) {
2159 var node = extractElementNode(container);
2160 callbackRegistry[event] = callbackRegistry[event] || [];
2161 callbackRegistry[event].push({
2162 node: node,
2163 callback: callback
2164 });
2165 },
2166
2167 off: function(event, container, callback) {
2168 var entries = callbackRegistry[event];
2169 if (!entries) return;
2170
2171 callbackRegistry[event] = arguments.length === 1
2172 ? null
2173 : filterFromRegistry(entries, container, callback);
2174
2175 function filterFromRegistry(list, matchContainer, matchCallback) {
2176 var containerNode = extractElementNode(matchContainer);
2177 return list.filter(function(entry) {
2178 var isMatch = entry.node === containerNode &&
2179 (!matchCallback || entry.callback === matchCallback);
2180 return !isMatch;
2181 });
2182 }
2183 },
2184
2185 pin: function(element, parentElement) {
2186 assertArg(isElement(element), 'element', 'not an element');
2187 assertArg(isElement(parentElement), 'parentElement', 'not an element');
2188 element.data(NG_ANIMATE_PIN_DATA, parentElement);
2189 },
2190
2191 push: function(element, event, options, domOperation) {
2192 options = options || {};
2193 options.domOperation = domOperation;
2194 return queueAnimation(element, event, options);
2195 },
2196
2197 // this method has four signatures:
2198 // () - global getter
2199 // (bool) - global setter
2200 // (element) - element getter
2201 // (element, bool) - element setter<F37>
2202 enabled: function(element, bool) {
2203 var argCount = arguments.length;
2204
2205 if (argCount === 0) {
2206 // () - Global getter
2207 bool = !!animationsEnabled;
2208 } else {
2209 var hasElement = isElement(element);
2210
2211 if (!hasElement) {
2212 // (bool) - Global setter
2213 bool = animationsEnabled = !!element;
2214 } else {
2215 var node = getDomNode(element);
2216 var recordExists = disabledElementsLookup.get(node);
2217
2218 if (argCount === 1) {
2219 // (element) - Element getter
2220 bool = !recordExists;
2221 } else {
2222 // (element, bool) - Element setter
2223 bool = !!bool;
2224 if (!bool) {
2225 disabledElementsLookup.put(node, true);
2226 } else if (recordExists) {
2227 disabledElementsLookup.remove(node);
2228 }
2229 }
2230 }
2231 }
2232
2233 return bool;
2234 }
2235 };
2236
2237 function queueAnimation(element, event, options) {
2238 var node, parent;
2239 element = stripCommentsFromElement(element);
2240 if (element) {
2241 node = getDomNode(element);
2242 parent = element.parent();
2243 }
2244
2245 options = prepareAnimationOptions(options);
2246
2247 // we create a fake runner with a working promise.
2248 // These methods will become available after the digest has passed
2249 var runner = new $$AnimateRunner();
2250
2251 // this is used to trigger callbacks in postDigest mode
2252 var runInNextPostDigestOrNow = postDigestTaskFactory();
2253
2254 if (isArray(options.addClass)) {
2255 options.addClass = options.addClass.join(' ');
2256 }
2257
2258 if (options.addClass && !isString(options.addClass)) {
2259 options.addClass = null;
2260 }
2261
2262 if (isArray(options.removeClass)) {
2263 options.removeClass = options.removeClass.join(' ');
2264 }
2265
2266 if (options.removeClass && !isString(options.removeClass)) {
2267 options.removeClass = null;
2268 }
2269
2270 if (options.from && !isObject(options.from)) {
2271 options.from = null;
2272 }
2273
2274 if (options.to && !isObject(options.to)) {
2275 options.to = null;
2276 }
2277
2278 // there are situations where a directive issues an animation for
2279 // a jqLite wrapper that contains only comment nodes... If this
2280 // happens then there is no way we can perform an animation
2281 if (!node) {
2282 close();
2283 return runner;
2284 }
2285
2286 var className = [node.className, options.addClass, options.removeClass].join(' ');
2287 if (!isAnimatableClassName(className)) {
2288 close();
2289 return runner;
2290 }
2291
2292 var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
2293
2294 // this is a hard disable of all animations for the application or on
2295 // the element itself, therefore there is no need to continue further
2296 // past this point if not enabled
2297 var skipAnimations = !animationsEnabled || disabledElementsLookup.get(node);
2298 var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
2299 var hasExistingAnimation = !!existingAnimation.state;
2300
2301 // there is no point in traversing the same collection of parent ancestors if a followup
2302 // animation will be run on the same element that already did all that checking work
2303 if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) {
2304 skipAnimations = !areAnimationsAllowed(element, parent, event);
2305 }
2306
2307 if (skipAnimations) {
2308 close();
2309 return runner;
2310 }
2311
2312 if (isStructural) {
2313 closeChildAnimations(element);
2314 }
2315
2316 var newAnimation = {
2317 structural: isStructural,
2318 element: element,
2319 event: event,
2320 close: close,
2321 options: options,
2322 runner: runner
2323 };
2324
2325 if (hasExistingAnimation) {
2326 var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation);
2327 if (skipAnimationFlag) {
2328 if (existingAnimation.state === RUNNING_STATE) {
2329 close();
2330 return runner;
2331 } else {
2332 mergeAnimationOptions(element, existingAnimation.options, options);
2333 return existingAnimation.runner;
2334 }
2335 }
2336
2337 var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation);
2338 if (cancelAnimationFlag) {
2339 if (existingAnimation.state === RUNNING_STATE) {
2340 // this will end the animation right away and it is safe
2341 // to do so since the animation is already running and the
2342 // runner callback code will run in async
2343 existingAnimation.runner.end();
2344 } else if (existingAnimation.structural) {
2345 // this means that the animation is queued into a digest, but
2346 // hasn't started yet. Therefore it is safe to run the close
2347 // method which will call the runner methods in async.
2348 existingAnimation.close();
2349 } else {
2350 // this will merge the new animation options into existing animation options
2351 mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
2352 return existingAnimation.runner;
2353 }
2354 } else {
2355 // a joined animation means that this animation will take over the existing one
2356 // so an example would involve a leave animation taking over an enter. Then when
2357 // the postDigest kicks in the enter will be ignored.
2358 var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation);
2359 if (joinAnimationFlag) {
2360 if (existingAnimation.state === RUNNING_STATE) {
2361 normalizeAnimationOptions(element, options);
2362 } else {
2363 applyGeneratedPreparationClasses(element, isStructural ? event : null, options);
2364
2365 event = newAnimation.event = existingAnimation.event;
2366 options = mergeAnimationOptions(element, existingAnimation.options, newAnimation.options);
2367
2368 //we return the same runner since only the option values of this animation will
2369 //be fed into the `existingAnimation`.
2370 return existingAnimation.runner;
2371 }
2372 }
2373 }
2374 } else {
2375 // normalization in this case means that it removes redundant CSS classes that
2376 // already exist (addClass) or do not exist (removeClass) on the element
2377 normalizeAnimationOptions(element, options);
2378 }
2379
2380 // when the options are merged and cleaned up we may end up not having to do
2381 // an animation at all, therefore we should check this before issuing a post
2382 // digest callback. Structural animations will always run no matter what.
2383 var isValidAnimation = newAnimation.structural;
2384 if (!isValidAnimation) {
2385 // animate (from/to) can be quickly checked first, otherwise we check if any classes are present
2386 isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0)
2387 || hasAnimationClasses(newAnimation.options);
2388 }
2389
2390 if (!isValidAnimation) {
2391 close();
2392 clearElementAnimationState(element);
2393 return runner;
2394 }
2395
2396 // the counter keeps track of cancelled animations
2397 var counter = (existingAnimation.counter || 0) + 1;
2398 newAnimation.counter = counter;
2399
2400 markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation);
2401
2402 $rootScope.$$postDigest(function() {
2403 var animationDetails = activeAnimationsLookup.get(node);
2404 var animationCancelled = !animationDetails;
2405 animationDetails = animationDetails || {};
2406
2407 // if addClass/removeClass is called before something like enter then the
2408 // registered parent element may not be present. The code below will ensure
2409 // that a final value for parent element is obtained
2410 var parentElement = element.parent() || [];
2411
2412 // animate/structural/class-based animations all have requirements. Otherwise there
2413 // is no point in performing an animation. The parent node must also be set.
2414 var isValidAnimation = parentElement.length > 0
2415 && (animationDetails.event === 'animate'
2416 || animationDetails.structural
2417 || hasAnimationClasses(animationDetails.options));
2418
2419 // this means that the previous animation was cancelled
2420 // even if the follow-up animation is the same event
2421 if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) {
2422 // if another animation did not take over then we need
2423 // to make sure that the domOperation and options are
2424 // handled accordingly
2425 if (animationCancelled) {
2426 applyAnimationClasses(element, options);
2427 applyAnimationStyles(element, options);
2428 }
2429
2430 // if the event changed from something like enter to leave then we do
2431 // it, otherwise if it's the same then the end result will be the same too
2432 if (animationCancelled || (isStructural && animationDetails.event !== event)) {
2433 options.domOperation();
2434 runner.end();
2435 }
2436
2437 // in the event that the element animation was not cancelled or a follow-up animation
2438 // isn't allowed to animate from here then we need to clear the state of the element
2439 // so that any future animations won't read the expired animation data.
2440 if (!isValidAnimation) {
2441 clearElementAnimationState(element);
2442 }
2443
2444 return;
2445 }
2446
2447 // this combined multiple class to addClass / removeClass into a setClass event
2448 // so long as a structural event did not take over the animation
2449 event = !animationDetails.structural && hasAnimationClasses(animationDetails.options, true)
2450 ? 'setClass'
2451 : animationDetails.event;
2452
2453 markElementAnimationState(element, RUNNING_STATE);
2454 var realRunner = $$animation(element, event, animationDetails.options);
2455
2456 realRunner.done(function(status) {
2457 close(!status);
2458 var animationDetails = activeAnimationsLookup.get(node);
2459 if (animationDetails && animationDetails.counter === counter) {
2460 clearElementAnimationState(getDomNode(element));
2461 }
2462 notifyProgress(runner, event, 'close', {});
2463 });
2464
2465 // this will update the runner's flow-control events based on
2466 // the `realRunner` object.
2467 runner.setHost(realRunner);
2468 notifyProgress(runner, event, 'start', {});
2469 });
2470
2471 return runner;
2472
2473 function notifyProgress(runner, event, phase, data) {
2474 runInNextPostDigestOrNow(function() {
2475 var callbacks = findCallbacks(parent, element, event);
2476 if (callbacks.length) {
2477 // do not optimize this call here to RAF because
2478 // we don't know how heavy the callback code here will
2479 // be and if this code is buffered then this can
2480 // lead to a performance regression.
2481 $$rAF(function() {
2482 forEach(callbacks, function(callback) {
2483 callback(element, phase, data);
2484 });
2485 });
2486 }
2487 });
2488 runner.progress(event, phase, data);
2489 }
2490
2491 function close(reject) { // jshint ignore:line
2492 clearGeneratedClasses(element, options);
2493 applyAnimationClasses(element, options);
2494 applyAnimationStyles(element, options);
2495 options.domOperation();
2496 runner.complete(!reject);
2497 }
2498 }
2499
2500 function closeChildAnimations(element) {
2501 var node = getDomNode(element);
2502 var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
2503 forEach(children, function(child) {
2504 var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
2505 var animationDetails = activeAnimationsLookup.get(child);
2506 switch (state) {
2507 case RUNNING_STATE:
2508 animationDetails.runner.end();
2509 /* falls through */
2510 case PRE_DIGEST_STATE:
2511 if (animationDetails) {
2512 activeAnimationsLookup.remove(child);
2513 }
2514 break;
2515 }
2516 });
2517 }
2518
2519 function clearElementAnimationState(element) {
2520 var node = getDomNode(element);
2521 node.removeAttribute(NG_ANIMATE_ATTR_NAME);
2522 activeAnimationsLookup.remove(node);
2523 }
2524
2525 function isMatchingElement(nodeOrElmA, nodeOrElmB) {
2526 return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
2527 }
2528
2529 function areAnimationsAllowed(element, parentElement, event) {
2530 var bodyElement = jqLite($document[0].body);
2531 var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
2532 var rootElementDetected = isMatchingElement(element, $rootElement);
2533 var parentAnimationDetected = false;
2534 var animateChildren;
2535
2536 var parentHost = element.data(NG_ANIMATE_PIN_DATA);
2537 if (parentHost) {
2538 parentElement = parentHost;
2539 }
2540
2541 while (parentElement && parentElement.length) {
2542 if (!rootElementDetected) {
2543 // angular doesn't want to attempt to animate elements outside of the application
2544 // therefore we need to ensure that the rootElement is an ancestor of the current element
2545 rootElementDetected = isMatchingElement(parentElement, $rootElement);
2546 }
2547
2548 var parentNode = parentElement[0];
2549 if (parentNode.nodeType !== ELEMENT_NODE) {
2550 // no point in inspecting the #document element
2551 break;
2552 }
2553
2554 var details = activeAnimationsLookup.get(parentNode) || {};
2555 // either an enter, leave or move animation will commence
2556 // therefore we can't allow any animations to take place
2557 // but if a parent animation is class-based then that's ok
2558 if (!parentAnimationDetected) {
2559 parentAnimationDetected = details.structural || disabledElementsLookup.get(parentNode);
2560 }
2561
2562 if (isUndefined(animateChildren) || animateChildren === true) {
2563 var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA);
2564 if (isDefined(value)) {
2565 animateChildren = value;
2566 }
2567 }
2568
2569 // there is no need to continue traversing at this point
2570 if (parentAnimationDetected && animateChildren === false) break;
2571
2572 if (!rootElementDetected) {
2573 // angular doesn't want to attempt to animate elements outside of the application
2574 // therefore we need to ensure that the rootElement is an ancestor of the current element
2575 rootElementDetected = isMatchingElement(parentElement, $rootElement);
2576 if (!rootElementDetected) {
2577 parentHost = parentElement.data(NG_ANIMATE_PIN_DATA);
2578 if (parentHost) {
2579 parentElement = parentHost;
2580 }
2581 }
2582 }
2583
2584 if (!bodyElementDetected) {
2585 // we also need to ensure that the element is or will be apart of the body element
2586 // otherwise it is pointless to even issue an animation to be rendered
2587 bodyElementDetected = isMatchingElement(parentElement, bodyElement);
2588 }
2589
2590 parentElement = parentElement.parent();
2591 }
2592
2593 var allowAnimation = !parentAnimationDetected || animateChildren;
2594 return allowAnimation && rootElementDetected && bodyElementDetected;
2595 }
2596
2597 function markElementAnimationState(element, state, details) {
2598 details = details || {};
2599 details.state = state;
2600
2601 var node = getDomNode(element);
2602 node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
2603
2604 var oldValue = activeAnimationsLookup.get(node);
2605 var newValue = oldValue
2606 ? extend(oldValue, details)
2607 : details;
2608 activeAnimationsLookup.put(node, newValue);
2609 }
2610 }];
2611}];
2612
2613var $$AnimateAsyncRunFactory = ['$$rAF', function($$rAF) {
2614 var waitQueue = [];
2615
2616 function waitForTick(fn) {
2617 waitQueue.push(fn);
2618 if (waitQueue.length > 1) return;
2619 $$rAF(function() {
2620 for (var i = 0; i < waitQueue.length; i++) {
2621 waitQueue[i]();
2622 }
2623 waitQueue = [];
2624 });
2625 }
2626
2627 return function() {
2628 var passed = false;
2629 waitForTick(function() {
2630 passed = true;
2631 });
2632 return function(callback) {
2633 passed ? callback() : waitForTick(callback);
2634 };
2635 };
2636}];
2637
2638var $$AnimateRunnerFactory = ['$q', '$sniffer', '$$animateAsyncRun',
2639 function($q, $sniffer, $$animateAsyncRun) {
2640
2641 var INITIAL_STATE = 0;
2642 var DONE_PENDING_STATE = 1;
2643 var DONE_COMPLETE_STATE = 2;
2644
2645 AnimateRunner.chain = function(chain, callback) {
2646 var index = 0;
2647
2648 next();
2649 function next() {
2650 if (index === chain.length) {
2651 callback(true);
2652 return;
2653 }
2654
2655 chain[index](function(response) {
2656 if (response === false) {
2657 callback(false);
2658 return;
2659 }
2660 index++;
2661 next();
2662 });
2663 }
2664 };
2665
2666 AnimateRunner.all = function(runners, callback) {
2667 var count = 0;
2668 var status = true;
2669 forEach(runners, function(runner) {
2670 runner.done(onProgress);
2671 });
2672
2673 function onProgress(response) {
2674 status = status && response;
2675 if (++count === runners.length) {
2676 callback(status);
2677 }
2678 }
2679 };
2680
2681 function AnimateRunner(host) {
2682 this.setHost(host);
2683
2684 this._doneCallbacks = [];
2685 this._runInAnimationFrame = $$animateAsyncRun();
2686 this._state = 0;
2687 }
2688
2689 AnimateRunner.prototype = {
2690 setHost: function(host) {
2691 this.host = host || {};
2692 },
2693
2694 done: function(fn) {
2695 if (this._state === DONE_COMPLETE_STATE) {
2696 fn();
2697 } else {
2698 this._doneCallbacks.push(fn);
2699 }
2700 },
2701
2702 progress: noop,
2703
2704 getPromise: function() {
2705 if (!this.promise) {
2706 var self = this;
2707 this.promise = $q(function(resolve, reject) {
2708 self.done(function(status) {
2709 status === false ? reject() : resolve();
2710 });
2711 });
2712 }
2713 return this.promise;
2714 },
2715
2716 then: function(resolveHandler, rejectHandler) {
2717 return this.getPromise().then(resolveHandler, rejectHandler);
2718 },
2719
2720 'catch': function(handler) {
2721 return this.getPromise()['catch'](handler);
2722 },
2723
2724 'finally': function(handler) {
2725 return this.getPromise()['finally'](handler);
2726 },
2727
2728 pause: function() {
2729 if (this.host.pause) {
2730 this.host.pause();
2731 }
2732 },
2733
2734 resume: function() {
2735 if (this.host.resume) {
2736 this.host.resume();
2737 }
2738 },
2739
2740 end: function() {
2741 if (this.host.end) {
2742 this.host.end();
2743 }
2744 this._resolve(true);
2745 },
2746
2747 cancel: function() {
2748 if (this.host.cancel) {
2749 this.host.cancel();
2750 }
2751 this._resolve(false);
2752 },
2753
2754 complete: function(response) {
2755 var self = this;
2756 if (self._state === INITIAL_STATE) {
2757 self._state = DONE_PENDING_STATE;
2758 self._runInAnimationFrame(function() {
2759 self._resolve(response);
2760 });
2761 }
2762 },
2763
2764 _resolve: function(response) {
2765 if (this._state !== DONE_COMPLETE_STATE) {
2766 forEach(this._doneCallbacks, function(fn) {
2767 fn(response);
2768 });
2769 this._doneCallbacks.length = 0;
2770 this._state = DONE_COMPLETE_STATE;
2771 }
2772 }
2773 };
2774
2775 return AnimateRunner;
2776}];
2777
2778var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
2779 var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
2780
2781 var drivers = this.drivers = [];
2782
2783 var RUNNER_STORAGE_KEY = '$$animationRunner';
2784
2785 function setRunner(element, runner) {
2786 element.data(RUNNER_STORAGE_KEY, runner);
2787 }
2788
2789 function removeRunner(element) {
2790 element.removeData(RUNNER_STORAGE_KEY);
2791 }
2792
2793 function getRunner(element) {
2794 return element.data(RUNNER_STORAGE_KEY);
2795 }
2796
2797 this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler',
2798 function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) {
2799
2800 var animationQueue = [];
2801 var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
2802
2803 function sortAnimations(animations) {
2804 var tree = { children: [] };
2805 var i, lookup = new $$HashMap();
2806
2807 // this is done first beforehand so that the hashmap
2808 // is filled with a list of the elements that will be animated
2809 for (i = 0; i < animations.length; i++) {
2810 var animation = animations[i];
2811 lookup.put(animation.domNode, animations[i] = {
2812 domNode: animation.domNode,
2813 fn: animation.fn,
2814 children: []
2815 });
2816 }
2817
2818 for (i = 0; i < animations.length; i++) {
2819 processNode(animations[i]);
2820 }
2821
2822 return flatten(tree);
2823
2824 function processNode(entry) {
2825 if (entry.processed) return entry;
2826 entry.processed = true;
2827
2828 var elementNode = entry.domNode;
2829 var parentNode = elementNode.parentNode;
2830 lookup.put(elementNode, entry);
2831
2832 var parentEntry;
2833 while (parentNode) {
2834 parentEntry = lookup.get(parentNode);
2835 if (parentEntry) {
2836 if (!parentEntry.processed) {
2837 parentEntry = processNode(parentEntry);
2838 }
2839 break;
2840 }
2841 parentNode = parentNode.parentNode;
2842 }
2843
2844 (parentEntry || tree).children.push(entry);
2845 return entry;
2846 }
2847
2848 function flatten(tree) {
2849 var result = [];
2850 var queue = [];
2851 var i;
2852
2853 for (i = 0; i < tree.children.length; i++) {
2854 queue.push(tree.children[i]);
2855 }
2856
2857 var remainingLevelEntries = queue.length;
2858 var nextLevelEntries = 0;
2859 var row = [];
2860
2861 for (i = 0; i < queue.length; i++) {
2862 var entry = queue[i];
2863 if (remainingLevelEntries <= 0) {
2864 remainingLevelEntries = nextLevelEntries;
2865 nextLevelEntries = 0;
2866 result.push(row);
2867 row = [];
2868 }
2869 row.push(entry.fn);
2870 entry.children.forEach(function(childEntry) {
2871 nextLevelEntries++;
2872 queue.push(childEntry);
2873 });
2874 remainingLevelEntries--;
2875 }
2876
2877 if (row.length) {
2878 result.push(row);
2879 }
2880
2881 return result;
2882 }
2883 }
2884
2885 // TODO(matsko): document the signature in a better way
2886 return function(element, event, options) {
2887 options = prepareAnimationOptions(options);
2888 var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
2889
2890 // there is no animation at the current moment, however
2891 // these runner methods will get later updated with the
2892 // methods leading into the driver's end/cancel methods
2893 // for now they just stop the animation from starting
2894 var runner = new $$AnimateRunner({
2895 end: function() { close(); },
2896 cancel: function() { close(true); }
2897 });
2898
2899 if (!drivers.length) {
2900 close();
2901 return runner;
2902 }
2903
2904 setRunner(element, runner);
2905
2906 var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass));
2907 var tempClasses = options.tempClasses;
2908 if (tempClasses) {
2909 classes += ' ' + tempClasses;
2910 options.tempClasses = null;
2911 }
2912
2913 animationQueue.push({
2914 // this data is used by the postDigest code and passed into
2915 // the driver step function
2916 element: element,
2917 classes: classes,
2918 event: event,
2919 structural: isStructural,
2920 options: options,
2921 beforeStart: beforeStart,
2922 close: close
2923 });
2924
2925 element.on('$destroy', handleDestroyedElement);
2926
2927 // we only want there to be one function called within the post digest
2928 // block. This way we can group animations for all the animations that
2929 // were apart of the same postDigest flush call.
2930 if (animationQueue.length > 1) return runner;
2931
2932 $rootScope.$$postDigest(function() {
2933 var animations = [];
2934 forEach(animationQueue, function(entry) {
2935 // the element was destroyed early on which removed the runner
2936 // form its storage. This means we can't animate this element
2937 // at all and it already has been closed due to destruction.
2938 if (getRunner(entry.element)) {
2939 animations.push(entry);
2940 } else {
2941 entry.close();
2942 }
2943 });
2944
2945 // now any future animations will be in another postDigest
2946 animationQueue.length = 0;
2947
2948 var groupedAnimations = groupAnimations(animations);
2949 var toBeSortedAnimations = [];
2950
2951 forEach(groupedAnimations, function(animationEntry) {
2952 toBeSortedAnimations.push({
2953 domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element),
2954 fn: function triggerAnimationStart() {
2955 // it's important that we apply the `ng-animate` CSS class and the
2956 // temporary classes before we do any driver invoking since these
2957 // CSS classes may be required for proper CSS detection.
2958 animationEntry.beforeStart();
2959
2960 var startAnimationFn, closeFn = animationEntry.close;
2961
2962 // in the event that the element was removed before the digest runs or
2963 // during the RAF sequencing then we should not trigger the animation.
2964 var targetElement = animationEntry.anchors
2965 ? (animationEntry.from.element || animationEntry.to.element)
2966 : animationEntry.element;
2967
2968 if (getRunner(targetElement)) {
2969 var operation = invokeFirstDriver(animationEntry);
2970 if (operation) {
2971 startAnimationFn = operation.start;
2972 }
2973 }
2974
2975 if (!startAnimationFn) {
2976 closeFn();
2977 } else {
2978 var animationRunner = startAnimationFn();
2979 animationRunner.done(function(status) {
2980 closeFn(!status);
2981 });
2982 updateAnimationRunners(animationEntry, animationRunner);
2983 }
2984 }
2985 });
2986 });
2987
2988 // we need to sort each of the animations in order of parent to child
2989 // relationships. This ensures that the child classes are applied at the
2990 // right time.
2991 $$rAFScheduler(sortAnimations(toBeSortedAnimations));
2992 });
2993
2994 return runner;
2995
2996 // TODO(matsko): change to reference nodes
2997 function getAnchorNodes(node) {
2998 var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']';
2999 var items = node.hasAttribute(NG_ANIMATE_REF_ATTR)
3000 ? [node]
3001 : node.querySelectorAll(SELECTOR);
3002 var anchors = [];
3003 forEach(items, function(node) {
3004 var attr = node.getAttribute(NG_ANIMATE_REF_ATTR);
3005 if (attr && attr.length) {
3006 anchors.push(node);
3007 }
3008 });
3009 return anchors;
3010 }
3011
3012 function groupAnimations(animations) {
3013 var preparedAnimations = [];
3014 var refLookup = {};
3015 forEach(animations, function(animation, index) {
3016 var element = animation.element;
3017 var node = getDomNode(element);
3018 var event = animation.event;
3019 var enterOrMove = ['enter', 'move'].indexOf(event) >= 0;
3020 var anchorNodes = animation.structural ? getAnchorNodes(node) : [];
3021
3022 if (anchorNodes.length) {
3023 var direction = enterOrMove ? 'to' : 'from';
3024
3025 forEach(anchorNodes, function(anchor) {
3026 var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR);
3027 refLookup[key] = refLookup[key] || {};
3028 refLookup[key][direction] = {
3029 animationID: index,
3030 element: jqLite(anchor)
3031 };
3032 });
3033 } else {
3034 preparedAnimations.push(animation);
3035 }
3036 });
3037
3038 var usedIndicesLookup = {};
3039 var anchorGroups = {};
3040 forEach(refLookup, function(operations, key) {
3041 var from = operations.from;
3042 var to = operations.to;
3043
3044 if (!from || !to) {
3045 // only one of these is set therefore we can't have an
3046 // anchor animation since all three pieces are required
3047 var index = from ? from.animationID : to.animationID;
3048 var indexKey = index.toString();
3049 if (!usedIndicesLookup[indexKey]) {
3050 usedIndicesLookup[indexKey] = true;
3051 preparedAnimations.push(animations[index]);
3052 }
3053 return;
3054 }
3055
3056 var fromAnimation = animations[from.animationID];
3057 var toAnimation = animations[to.animationID];
3058 var lookupKey = from.animationID.toString();
3059 if (!anchorGroups[lookupKey]) {
3060 var group = anchorGroups[lookupKey] = {
3061 structural: true,
3062 beforeStart: function() {
3063 fromAnimation.beforeStart();
3064 toAnimation.beforeStart();
3065 },
3066 close: function() {
3067 fromAnimation.close();
3068 toAnimation.close();
3069 },
3070 classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes),
3071 from: fromAnimation,
3072 to: toAnimation,
3073 anchors: [] // TODO(matsko): change to reference nodes
3074 };
3075
3076 // the anchor animations require that the from and to elements both have at least
3077 // one shared CSS class which effictively marries the two elements together to use
3078 // the same animation driver and to properly sequence the anchor animation.
3079 if (group.classes.length) {
3080 preparedAnimations.push(group);
3081 } else {
3082 preparedAnimations.push(fromAnimation);
3083 preparedAnimations.push(toAnimation);
3084 }
3085 }
3086
3087 anchorGroups[lookupKey].anchors.push({
3088 'out': from.element, 'in': to.element
3089 });
3090 });
3091
3092 return preparedAnimations;
3093 }
3094
3095 function cssClassesIntersection(a,b) {
3096 a = a.split(' ');
3097 b = b.split(' ');
3098 var matches = [];
3099
3100 for (var i = 0; i < a.length; i++) {
3101 var aa = a[i];
3102 if (aa.substring(0,3) === 'ng-') continue;
3103
3104 for (var j = 0; j < b.length; j++) {
3105 if (aa === b[j]) {
3106 matches.push(aa);
3107 break;
3108 }
3109 }
3110 }
3111
3112 return matches.join(' ');
3113 }
3114
3115 function invokeFirstDriver(animationDetails) {
3116 // we loop in reverse order since the more general drivers (like CSS and JS)
3117 // may attempt more elements, but custom drivers are more particular
3118 for (var i = drivers.length - 1; i >= 0; i--) {
3119 var driverName = drivers[i];
3120 if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check
3121
3122 var factory = $injector.get(driverName);
3123 var driver = factory(animationDetails);
3124 if (driver) {
3125 return driver;
3126 }
3127 }
3128 }
3129
3130 function beforeStart() {
3131 element.addClass(NG_ANIMATE_CLASSNAME);
3132 if (tempClasses) {
3133 $$jqLite.addClass(element, tempClasses);
3134 }
3135 }
3136
3137 function updateAnimationRunners(animation, newRunner) {
3138 if (animation.from && animation.to) {
3139 update(animation.from.element);
3140 update(animation.to.element);
3141 } else {
3142 update(animation.element);
3143 }
3144
3145 function update(element) {
3146 getRunner(element).setHost(newRunner);
3147 }
3148 }
3149
3150 function handleDestroyedElement() {
3151 var runner = getRunner(element);
3152 if (runner && (event !== 'leave' || !options.$$domOperationFired)) {
3153 runner.end();
3154 }
3155 }
3156
3157 function close(rejected) { // jshint ignore:line
3158 element.off('$destroy', handleDestroyedElement);
3159 removeRunner(element);
3160
3161 applyAnimationClasses(element, options);
3162 applyAnimationStyles(element, options);
3163 options.domOperation();
3164
3165 if (tempClasses) {
3166 $$jqLite.removeClass(element, tempClasses);
3167 }
3168
3169 element.removeClass(NG_ANIMATE_CLASSNAME);
3170 runner.complete(!rejected);
3171 }
3172 };
3173 }];
3174}];
3175
3176/* global angularAnimateModule: true,
3177
3178 $$AnimateAsyncRunFactory,
3179 $$rAFSchedulerFactory,
3180 $$AnimateChildrenDirective,
3181 $$AnimateRunnerFactory,
3182 $$AnimateQueueProvider,
3183 $$AnimationProvider,
3184 $AnimateCssProvider,
3185 $$AnimateCssDriverProvider,
3186 $$AnimateJsProvider,
3187 $$AnimateJsDriverProvider,
3188*/
3189
3190/**
3191 * @ngdoc module
3192 * @name ngAnimate
3193 * @description
3194 *
3195 * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via
3196 * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app.
3197 *
3198 * <div doc-module-components="ngAnimate"></div>
3199 *
3200 * # Usage
3201 * 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
3202 * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For
3203 * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within
3204 * the HTML element that the animation will be triggered on.
3205 *
3206 * ## Directive Support
3207 * The following directives are "animation aware":
3208 *
3209 * | Directive | Supported Animations |
3210 * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
3211 * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move |
3212 * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
3213 * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave |
3214 * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave |
3215 * | {@link ng.directive:ngIf#animations ngIf} | enter and leave |
3216 * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) |
3217 * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) |
3218 * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) |
3219 * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) |
3220 * | {@link module:ngMessages#animations ngMessage} | enter and leave |
3221 *
3222 * (More information can be found by visiting each the documentation associated with each directive.)
3223 *
3224 * ## CSS-based Animations
3225 *
3226 * 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
3227 * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation.
3228 *
3229 * The example below shows how an `enter` animation can be made possible on an element using `ng-if`:
3230 *
3231 * ```html
3232 * <div ng-if="bool" class="fade">
3233 * Fade me in out
3234 * </div>
3235 * <button ng-click="bool=true">Fade In!</button>
3236 * <button ng-click="bool=false">Fade Out!</button>
3237 * ```
3238 *
3239 * Notice the CSS class **fade**? We can now create the CSS transition code that references this class:
3240 *
3241 * ```css
3242 * /&#42; The starting CSS styles for the enter animation &#42;/
3243 * .fade.ng-enter {
3244 * transition:0.5s linear all;
3245 * opacity:0;
3246 * }
3247 *
3248 * /&#42; The finishing CSS styles for the enter animation &#42;/
3249 * .fade.ng-enter.ng-enter-active {
3250 * opacity:1;
3251 * }
3252 * ```
3253 *
3254 * 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
3255 * 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
3256 * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards.
3257 *
3258 * 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:
3259 *
3260 * ```css
3261 * /&#42; now the element will fade out before it is removed from the DOM &#42;/
3262 * .fade.ng-leave {
3263 * transition:0.5s linear all;
3264 * opacity:1;
3265 * }
3266 * .fade.ng-leave.ng-leave-active {
3267 * opacity:0;
3268 * }
3269 * ```
3270 *
3271 * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class:
3272 *
3273 * ```css
3274 * /&#42; there is no need to define anything inside of the destination
3275 * CSS class since the keyframe will take charge of the animation &#42;/
3276 * .fade.ng-leave {
3277 * animation: my_fade_animation 0.5s linear;
3278 * -webkit-animation: my_fade_animation 0.5s linear;
3279 * }
3280 *
3281 * @keyframes my_fade_animation {
3282 * from { opacity:1; }
3283 * to { opacity:0; }
3284 * }
3285 *
3286 * @-webkit-keyframes my_fade_animation {
3287 * from { opacity:1; }
3288 * to { opacity:0; }
3289 * }
3290 * ```
3291 *
3292 * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element.
3293 *
3294 * ### CSS Class-based Animations
3295 *
3296 * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different
3297 * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added
3298 * and removed.
3299 *
3300 * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class:
3301 *
3302 * ```html
3303 * <div ng-show="bool" class="fade">
3304 * Show and hide me
3305 * </div>
3306 * <button ng-click="bool=true">Toggle</button>
3307 *
3308 * <style>
3309 * .fade.ng-hide {
3310 * transition:0.5s linear all;
3311 * opacity:0;
3312 * }
3313 * </style>
3314 * ```
3315 *
3316 * 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
3317 * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest.
3318 *
3319 * 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
3320 * with CSS styles.
3321 *
3322 * ```html
3323 * <div ng-class="{on:onOff}" class="highlight">
3324 * Highlight this box
3325 * </div>
3326 * <button ng-click="onOff=!onOff">Toggle</button>
3327 *
3328 * <style>
3329 * .highlight {
3330 * transition:0.5s linear all;
3331 * }
3332 * .highlight.on-add {
3333 * background:white;
3334 * }
3335 * .highlight.on {
3336 * background:yellow;
3337 * }
3338 * .highlight.on-remove {
3339 * background:black;
3340 * }
3341 * </style>
3342 * ```
3343 *
3344 * We can also make use of CSS keyframes by placing them within the CSS classes.
3345 *
3346 *
3347 * ### CSS Staggering Animations
3348 * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a
3349 * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be
3350 * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for
3351 * the animation. The style property expected within the stagger class can either be a **transition-delay** or an
3352 * **animation-delay** property (or both if your animation contains both transitions and keyframe animations).
3353 *
3354 * ```css
3355 * .my-animation.ng-enter {
3356 * /&#42; standard transition code &#42;/
3357 * transition: 1s linear all;
3358 * opacity:0;
3359 * }
3360 * .my-animation.ng-enter-stagger {
3361 * /&#42; this will have a 100ms delay between each successive leave animation &#42;/
3362 * transition-delay: 0.1s;
3363 *
3364 * /&#42; As of 1.4.4, this must always be set: it signals ngAnimate
3365 * to not accidentally inherit a delay property from another CSS class &#42;/
3366 * transition-duration: 0s;
3367 * }
3368 * .my-animation.ng-enter.ng-enter-active {
3369 * /&#42; standard transition styles &#42;/
3370 * opacity:1;
3371 * }
3372 * ```
3373 *
3374 * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations
3375 * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this
3376 * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation
3377 * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired.
3378 *
3379 * The following code will issue the **ng-leave-stagger** event on the element provided:
3380 *
3381 * ```js
3382 * var kids = parent.children();
3383 *
3384 * $animate.leave(kids[0]); //stagger index=0
3385 * $animate.leave(kids[1]); //stagger index=1
3386 * $animate.leave(kids[2]); //stagger index=2
3387 * $animate.leave(kids[3]); //stagger index=3
3388 * $animate.leave(kids[4]); //stagger index=4
3389 *
3390 * window.requestAnimationFrame(function() {
3391 * //stagger has reset itself
3392 * $animate.leave(kids[5]); //stagger index=0
3393 * $animate.leave(kids[6]); //stagger index=1
3394 *
3395 * $scope.$digest();
3396 * });
3397 * ```
3398 *
3399 * Stagger animations are currently only supported within CSS-defined animations.
3400 *
3401 * ### The `ng-animate` CSS class
3402 *
3403 * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation.
3404 * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations).
3405 *
3406 * Therefore, animations can be applied to an element using this temporary class directly via CSS.
3407 *
3408 * ```css
3409 * .zipper.ng-animate {
3410 * transition:0.5s linear all;
3411 * }
3412 * .zipper.ng-enter {
3413 * opacity:0;
3414 * }
3415 * .zipper.ng-enter.ng-enter-active {
3416 * opacity:1;
3417 * }
3418 * .zipper.ng-leave {
3419 * opacity:1;
3420 * }
3421 * .zipper.ng-leave.ng-leave-active {
3422 * opacity:0;
3423 * }
3424 * ```
3425 *
3426 * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove
3427 * the CSS class once an animation has completed.)
3428 *
3429 *
3430 * ## JavaScript-based Animations
3431 *
3432 * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
3433 * 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
3434 * `module.animation()` module function we can register the ainmation.
3435 *
3436 * Let's see an example of a enter/leave animation using `ngRepeat`:
3437 *
3438 * ```html
3439 * <div ng-repeat="item in items" class="slide">
3440 * {{ item }}
3441 * </div>
3442 * ```
3443 *
3444 * 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`:
3445 *
3446 * ```js
3447 * myModule.animation('.slide', [function() {
3448 * return {
3449 * // make note that other events (like addClass/removeClass)
3450 * // have different function input parameters
3451 * enter: function(element, doneFn) {
3452 * jQuery(element).fadeIn(1000, doneFn);
3453 *
3454 * // remember to call doneFn so that angular
3455 * // knows that the animation has concluded
3456 * },
3457 *
3458 * move: function(element, doneFn) {
3459 * jQuery(element).fadeIn(1000, doneFn);
3460 * },
3461 *
3462 * leave: function(element, doneFn) {
3463 * jQuery(element).fadeOut(1000, doneFn);
3464 * }
3465 * }
3466 * }]);
3467 * ```
3468 *
3469 * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
3470 * greensock.js and velocity.js.
3471 *
3472 * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define
3473 * our animations inside of the same registered animation, however, the function input arguments are a bit different:
3474 *
3475 * ```html
3476 * <div ng-class="color" class="colorful">
3477 * this box is moody
3478 * </div>
3479 * <button ng-click="color='red'">Change to red</button>
3480 * <button ng-click="color='blue'">Change to blue</button>
3481 * <button ng-click="color='green'">Change to green</button>
3482 * ```
3483 *
3484 * ```js
3485 * myModule.animation('.colorful', [function() {
3486 * return {
3487 * addClass: function(element, className, doneFn) {
3488 * // do some cool animation and call the doneFn
3489 * },
3490 * removeClass: function(element, className, doneFn) {
3491 * // do some cool animation and call the doneFn
3492 * },
3493 * setClass: function(element, addedClass, removedClass, doneFn) {
3494 * // do some cool animation and call the doneFn
3495 * }
3496 * }
3497 * }]);
3498 * ```
3499 *
3500 * ## CSS + JS Animations Together
3501 *
3502 * 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,
3503 * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking
3504 * charge of the animation**:
3505 *
3506 * ```html
3507 * <div ng-if="bool" class="slide">
3508 * Slide in and out
3509 * </div>
3510 * ```
3511 *
3512 * ```js
3513 * myModule.animation('.slide', [function() {
3514 * return {
3515 * enter: function(element, doneFn) {
3516 * jQuery(element).slideIn(1000, doneFn);
3517 * }
3518 * }
3519 * }]);
3520 * ```
3521 *
3522 * ```css
3523 * .slide.ng-enter {
3524 * transition:0.5s linear all;
3525 * transform:translateY(-100px);
3526 * }
3527 * .slide.ng-enter.ng-enter-active {
3528 * transform:translateY(0);
3529 * }
3530 * ```
3531 *
3532 * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the
3533 * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from
3534 * our own JS-based animation code:
3535 *
3536 * ```js
3537 * myModule.animation('.slide', ['$animateCss', function($animateCss) {
3538 * return {
3539 * enter: function(element) {
3540* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
3541 * return $animateCss(element, {
3542 * event: 'enter',
3543 * structural: true
3544 * });
3545 * }
3546 * }
3547 * }]);
3548 * ```
3549 *
3550 * 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.
3551 *
3552 * 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
3553 * 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
3554 * data into `$animateCss` directly:
3555 *
3556 * ```js
3557 * myModule.animation('.slide', ['$animateCss', function($animateCss) {
3558 * return {
3559 * enter: function(element) {
3560 * return $animateCss(element, {
3561 * event: 'enter',
3562 * structural: true,
3563 * addClass: 'maroon-setting',
3564 * from: { height:0 },
3565 * to: { height: 200 }
3566 * });
3567 * }
3568 * }
3569 * }]);
3570 * ```
3571 *
3572 * Now we can fill in the rest via our transition CSS code:
3573 *
3574 * ```css
3575 * /&#42; the transition tells ngAnimate to make the animation happen &#42;/
3576 * .slide.ng-enter { transition:0.5s linear all; }
3577 *
3578 * /&#42; this extra CSS class will be absorbed into the transition
3579 * since the $animateCss code is adding the class &#42;/
3580 * .maroon-setting { background:red; }
3581 * ```
3582 *
3583 * 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.
3584 *
3585 * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}.
3586 *
3587 * ## Animation Anchoring (via `ng-animate-ref`)
3588 *
3589 * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between
3590 * structural areas of an application (like views) by pairing up elements using an attribute
3591 * called `ng-animate-ref`.
3592 *
3593 * Let's say for example we have two views that are managed by `ng-view` and we want to show
3594 * that there is a relationship between two components situated in within these views. By using the
3595 * `ng-animate-ref` attribute we can identify that the two components are paired together and we
3596 * can then attach an animation, which is triggered when the view changes.
3597 *
3598 * Say for example we have the following template code:
3599 *
3600 * ```html
3601 * <!-- index.html -->
3602 * <div ng-view class="view-animation">
3603 * </div>
3604 *
3605 * <!-- home.html -->
3606 * <a href="#/banner-page">
3607 * <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
3608 * </a>
3609 *
3610 * <!-- banner-page.html -->
3611 * <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
3612 * ```
3613 *
3614 * Now, when the view changes (once the link is clicked), ngAnimate will examine the
3615 * HTML contents to see if there is a match reference between any components in the view
3616 * that is leaving and the view that is entering. It will scan both the view which is being
3617 * removed (leave) and inserted (enter) to see if there are any paired DOM elements that
3618 * contain a matching ref value.
3619 *
3620 * The two images match since they share the same ref value. ngAnimate will now create a
3621 * transport element (which is a clone of the first image element) and it will then attempt
3622 * to animate to the position of the second image element in the next view. For the animation to
3623 * work a special CSS class called `ng-anchor` will be added to the transported element.
3624 *
3625 * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then
3626 * ngAnimate will handle the entire transition for us as well as the addition and removal of
3627 * any changes of CSS classes between the elements:
3628 *
3629 * ```css
3630 * .banner.ng-anchor {
3631 * /&#42; this animation will last for 1 second since there are
3632 * two phases to the animation (an `in` and an `out` phase) &#42;/
3633 * transition:0.5s linear all;
3634 * }
3635 * ```
3636 *
3637 * We also **must** include animations for the views that are being entered and removed
3638 * (otherwise anchoring wouldn't be possible since the new view would be inserted right away).
3639 *
3640 * ```css
3641 * .view-animation.ng-enter, .view-animation.ng-leave {
3642 * transition:0.5s linear all;
3643 * position:fixed;
3644 * left:0;
3645 * top:0;
3646 * width:100%;
3647 * }
3648 * .view-animation.ng-enter {
3649 * transform:translateX(100%);
3650 * }
3651 * .view-animation.ng-leave,
3652 * .view-animation.ng-enter.ng-enter-active {
3653 * transform:translateX(0%);
3654 * }
3655 * .view-animation.ng-leave.ng-leave-active {
3656 * transform:translateX(-100%);
3657 * }
3658 * ```
3659 *
3660 * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur:
3661 * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away
3662 * from its origin. Once that animation is over then the `in` stage occurs which animates the
3663 * element to its destination. The reason why there are two animations is to give enough time
3664 * for the enter animation on the new element to be ready.
3665 *
3666 * The example above sets up a transition for both the in and out phases, but we can also target the out or
3667 * in phases directly via `ng-anchor-out` and `ng-anchor-in`.
3668 *
3669 * ```css
3670 * .banner.ng-anchor-out {
3671 * transition: 0.5s linear all;
3672 *
3673 * /&#42; the scale will be applied during the out animation,
3674 * but will be animated away when the in animation runs &#42;/
3675 * transform: scale(1.2);
3676 * }
3677 *
3678 * .banner.ng-anchor-in {
3679 * transition: 1s linear all;
3680 * }
3681 * ```
3682 *
3683 *
3684 *
3685 *
3686 * ### Anchoring Demo
3687 *
3688 <example module="anchoringExample"
3689 name="anchoringExample"
3690 id="anchoringExample"
3691 deps="angular-animate.js;angular-route.js"
3692 animations="true">
3693 <file name="index.html">
3694 <a href="#/">Home</a>
3695 <hr />
3696 <div class="view-container">
3697 <div ng-view class="view"></div>
3698 </div>
3699 </file>
3700 <file name="script.js">
3701 angular.module('anchoringExample', ['ngAnimate', 'ngRoute'])
3702 .config(['$routeProvider', function($routeProvider) {
3703 $routeProvider.when('/', {
3704 templateUrl: 'home.html',
3705 controller: 'HomeController as home'
3706 });
3707 $routeProvider.when('/profile/:id', {
3708 templateUrl: 'profile.html',
3709 controller: 'ProfileController as profile'
3710 });
3711 }])
3712 .run(['$rootScope', function($rootScope) {
3713 $rootScope.records = [
3714 { id:1, title: "Miss Beulah Roob" },
3715 { id:2, title: "Trent Morissette" },
3716 { id:3, title: "Miss Ava Pouros" },
3717 { id:4, title: "Rod Pouros" },
3718 { id:5, title: "Abdul Rice" },
3719 { id:6, title: "Laurie Rutherford Sr." },
3720 { id:7, title: "Nakia McLaughlin" },
3721 { id:8, title: "Jordon Blanda DVM" },
3722 { id:9, title: "Rhoda Hand" },
3723 { id:10, title: "Alexandrea Sauer" }
3724 ];
3725 }])
3726 .controller('HomeController', [function() {
3727 //empty
3728 }])
3729 .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) {
3730 var index = parseInt($routeParams.id, 10);
3731 var record = $rootScope.records[index - 1];
3732
3733 this.title = record.title;
3734 this.id = record.id;
3735 }]);
3736 </file>
3737 <file name="home.html">
3738 <h2>Welcome to the home page</h1>
3739 <p>Please click on an element</p>
3740 <a class="record"
3741 ng-href="#/profile/{{ record.id }}"
3742 ng-animate-ref="{{ record.id }}"
3743 ng-repeat="record in records">
3744 {{ record.title }}
3745 </a>
3746 </file>
3747 <file name="profile.html">
3748 <div class="profile record" ng-animate-ref="{{ profile.id }}">
3749 {{ profile.title }}
3750 </div>
3751 </file>
3752 <file name="animations.css">
3753 .record {
3754 display:block;
3755 font-size:20px;
3756 }
3757 .profile {
3758 background:black;
3759 color:white;
3760 font-size:100px;
3761 }
3762 .view-container {
3763 position:relative;
3764 }
3765 .view-container > .view.ng-animate {
3766 position:absolute;
3767 top:0;
3768 left:0;
3769 width:100%;
3770 min-height:500px;
3771 }
3772 .view.ng-enter, .view.ng-leave,
3773 .record.ng-anchor {
3774 transition:0.5s linear all;
3775 }
3776 .view.ng-enter {
3777 transform:translateX(100%);
3778 }
3779 .view.ng-enter.ng-enter-active, .view.ng-leave {
3780 transform:translateX(0%);
3781 }
3782 .view.ng-leave.ng-leave-active {
3783 transform:translateX(-100%);
3784 }
3785 .record.ng-anchor-out {
3786 background:red;
3787 }
3788 </file>
3789 </example>
3790 *
3791 * ### How is the element transported?
3792 *
3793 * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting
3794 * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element
3795 * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The
3796 * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match
3797 * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied
3798 * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class
3799 * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element
3800 * will become visible since the shim class will be removed.
3801 *
3802 * ### How is the morphing handled?
3803 *
3804 * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out
3805 * what CSS classes differ between the starting element and the destination element. These different CSS classes
3806 * will be added/removed on the anchor element and a transition will be applied (the transition that is provided
3807 * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will
3808 * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that
3809 * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since
3810 * the cloned element is placed inside of root element which is likely close to the body element).
3811 *
3812 * Note that if the root element is on the `<html>` element then the cloned node will be placed inside of body.
3813 *
3814 *
3815 * ## Using $animate in your directive code
3816 *
3817 * 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?
3818 * 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
3819 * imagine we have a greeting box that shows and hides itself when the data changes
3820 *
3821 * ```html
3822 * <greeting-box active="onOrOff">Hi there</greeting-box>
3823 * ```
3824 *
3825 * ```js
3826 * ngModule.directive('greetingBox', ['$animate', function($animate) {
3827 * return function(scope, element, attrs) {
3828 * attrs.$observe('active', function(value) {
3829 * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on');
3830 * });
3831 * });
3832 * }]);
3833 * ```
3834 *
3835 * 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
3836 * in our HTML code then we can trigger a CSS or JS animation to happen.
3837 *
3838 * ```css
3839 * /&#42; normally we would create a CSS class to reference on the element &#42;/
3840 * greeting-box.on { transition:0.5s linear all; background:green; color:white; }
3841 * ```
3842 *
3843 * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's
3844 * possible be sure to visit the {@link ng.$animate $animate service API page}.
3845 *
3846 *
3847 * ### Preventing Collisions With Third Party Libraries
3848 *
3849 * Some third-party frameworks place animation duration defaults across many element or className
3850 * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which
3851 * is expecting actual animations on these elements and has to wait for their completion.
3852 *
3853 * You can prevent this unwanted behavior by using a prefix on all your animation classes:
3854 *
3855 * ```css
3856 * /&#42; prefixed with animate- &#42;/
3857 * .animate-fade-add.animate-fade-add-active {
3858 * transition:1s linear all;
3859 * opacity:0;
3860 * }
3861 * ```
3862 *
3863 * You then configure `$animate` to enforce this prefix:
3864 *
3865 * ```js
3866 * $animateProvider.classNameFilter(/animate-/);
3867 * ```
3868 *
3869 * This also may provide your application with a speed boost since only specific elements containing CSS class prefix
3870 * will be evaluated for animation when any DOM changes occur in the application.
3871 *
3872 * ## Callbacks and Promises
3873 *
3874 * 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
3875 * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has
3876 * ended by chaining onto the returned promise that animation method returns.
3877 *
3878 * ```js
3879 * // somewhere within the depths of the directive
3880 * $animate.enter(element, parent).then(function() {
3881 * //the animation has completed
3882 * });
3883 * ```
3884 *
3885 * (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
3886 * anymore.)
3887 *
3888 * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering
3889 * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view
3890 * routing controller to hook into that:
3891 *
3892 * ```js
3893 * ngModule.controller('HomePageController', ['$animate', function($animate) {
3894 * $animate.on('enter', ngViewElement, function(element) {
3895 * // the animation for this route has completed
3896 * }]);
3897 * }])
3898 * ```
3899 *
3900 * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.)
3901 */
3902
3903/**
3904 * @ngdoc service
3905 * @name $animate
3906 * @kind object
3907 *
3908 * @description
3909 * The ngAnimate `$animate` service documentation is the same for the core `$animate` service.
3910 *
3911 * Click here {@link ng.$animate to learn more about animations with `$animate`}.
3912 */
3913angular.module('ngAnimate', [])
3914 .directive('ngAnimateChildren', $$AnimateChildrenDirective)
3915 .factory('$$rAFScheduler', $$rAFSchedulerFactory)
3916
3917 .factory('$$AnimateRunner', $$AnimateRunnerFactory)
3918 .factory('$$animateAsyncRun', $$AnimateAsyncRunFactory)
3919
3920 .provider('$$animateQueue', $$AnimateQueueProvider)
3921 .provider('$$animation', $$AnimationProvider)
3922
3923 .provider('$animateCss', $AnimateCssProvider)
3924 .provider('$$animateCssDriver', $$AnimateCssDriverProvider)
3925
3926 .provider('$$animateJs', $$AnimateJsProvider)
3927 .provider('$$animateJsDriver', $$AnimateJsDriverProvider);
3928
3929
3930})(window, window.angular);