UNPKG

ngipstack

Version:

A set of AngularJS directives/helpers for using IpStack with AngularJS

1,366 lines (1,192 loc) 151 kB
/** * @license AngularJS v1.7.0 * (c) 2010-2018 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular) {'use strict'; var ELEMENT_NODE = 1; var COMMENT_NODE = 8; var ADD_CLASS_SUFFIX = '-add'; var REMOVE_CLASS_SUFFIX = '-remove'; var EVENT_CLASS_PREFIX = 'ng-'; var ACTIVE_CLASS_SUFFIX = '-active'; var PREPARE_CLASS_SUFFIX = '-prepare'; var NG_ANIMATE_CLASSNAME = 'ng-animate'; var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; // Detect proper transitionend/animationend event names. var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; // If unprefixed events are not supported but webkit-prefixed are, use the latter. // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. // Register both events in case `window.onanimationend` is not supported because of that, // do the same for `transitionend` as Safari is likely to exhibit similar behavior. // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit // therefore there is no reason to test anymore for other vendor prefixes: // http://caniuse.com/#search=transition if ((window.ontransitionend === undefined) && (window.onwebkittransitionend !== undefined)) { CSS_PREFIX = '-webkit-'; TRANSITION_PROP = 'WebkitTransition'; TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; } else { TRANSITION_PROP = 'transition'; TRANSITIONEND_EVENT = 'transitionend'; } if ((window.onanimationend === undefined) && (window.onwebkitanimationend !== undefined)) { CSS_PREFIX = '-webkit-'; ANIMATION_PROP = 'WebkitAnimation'; ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; } else { ANIMATION_PROP = 'animation'; ANIMATIONEND_EVENT = 'animationend'; } var DURATION_KEY = 'Duration'; var PROPERTY_KEY = 'Property'; var DELAY_KEY = 'Delay'; var TIMING_KEY = 'TimingFunction'; var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; var ANIMATION_PLAYSTATE_KEY = 'PlayState'; var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; var ngMinErr = angular.$$minErr('ng'); function assertArg(arg, name, reason) { if (!arg) { throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } function mergeClasses(a,b) { if (!a && !b) return ''; if (!a) return b; if (!b) return a; if (isArray(a)) a = a.join(' '); if (isArray(b)) b = b.join(' '); return a + ' ' + b; } function packageStyles(options) { var styles = {}; if (options && (options.to || options.from)) { styles.to = options.to; styles.from = options.from; } return styles; } function pendClasses(classes, fix, isPrefix) { var className = ''; classes = isArray(classes) ? classes : classes && isString(classes) && classes.length ? classes.split(/\s+/) : []; forEach(classes, function(klass, i) { if (klass && klass.length > 0) { className += (i > 0) ? ' ' : ''; className += isPrefix ? fix + klass : klass + fix; } }); return className; } function removeFromArray(arr, val) { var index = arr.indexOf(val); if (val >= 0) { arr.splice(index, 1); } } function stripCommentsFromElement(element) { if (element instanceof jqLite) { switch (element.length) { case 0: return element; case 1: // there is no point of stripping anything if the element // is the only element within the jqLite wrapper. // (it's important that we retain the element instance.) if (element[0].nodeType === ELEMENT_NODE) { return element; } break; default: return jqLite(extractElementNode(element)); } } if (element.nodeType === ELEMENT_NODE) { return jqLite(element); } } function extractElementNode(element) { if (!element[0]) return element; for (var i = 0; i < element.length; i++) { var elm = element[i]; if (elm.nodeType === ELEMENT_NODE) { return elm; } } } function $$addClass($$jqLite, element, className) { forEach(element, function(elm) { $$jqLite.addClass(elm, className); }); } function $$removeClass($$jqLite, element, className) { forEach(element, function(elm) { $$jqLite.removeClass(elm, className); }); } function applyAnimationClassesFactory($$jqLite) { return function(element, options) { if (options.addClass) { $$addClass($$jqLite, element, options.addClass); options.addClass = null; } if (options.removeClass) { $$removeClass($$jqLite, element, options.removeClass); options.removeClass = null; } }; } function prepareAnimationOptions(options) { options = options || {}; if (!options.$$prepared) { var domOperation = options.domOperation || noop; options.domOperation = function() { options.$$domOperationFired = true; domOperation(); domOperation = noop; }; options.$$prepared = true; } return options; } function applyAnimationStyles(element, options) { applyAnimationFromStyles(element, options); applyAnimationToStyles(element, options); } function applyAnimationFromStyles(element, options) { if (options.from) { element.css(options.from); options.from = null; } } function applyAnimationToStyles(element, options) { if (options.to) { element.css(options.to); options.to = null; } } function mergeAnimationDetails(element, oldAnimation, newAnimation) { var target = oldAnimation.options || {}; var newOptions = newAnimation.options || {}; var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || ''); var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || ''); var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove); if (newOptions.preparationClasses) { target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses); delete newOptions.preparationClasses; } // noop is basically when there is no callback; otherwise something has been set var realDomOperation = target.domOperation !== noop ? target.domOperation : null; extend(target, newOptions); // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this. if (realDomOperation) { target.domOperation = realDomOperation; } if (classes.addClass) { target.addClass = classes.addClass; } else { target.addClass = null; } if (classes.removeClass) { target.removeClass = classes.removeClass; } else { target.removeClass = null; } oldAnimation.addClass = target.addClass; oldAnimation.removeClass = target.removeClass; return target; } function resolveElementClasses(existing, toAdd, toRemove) { var ADD_CLASS = 1; var REMOVE_CLASS = -1; var flags = {}; existing = splitClassesToLookup(existing); toAdd = splitClassesToLookup(toAdd); forEach(toAdd, function(value, key) { flags[key] = ADD_CLASS; }); toRemove = splitClassesToLookup(toRemove); forEach(toRemove, function(value, key) { flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS; }); var classes = { addClass: '', removeClass: '' }; forEach(flags, function(val, klass) { var prop, allow; if (val === ADD_CLASS) { prop = 'addClass'; allow = !existing[klass] || existing[klass + REMOVE_CLASS_SUFFIX]; } else if (val === REMOVE_CLASS) { prop = 'removeClass'; allow = existing[klass] || existing[klass + ADD_CLASS_SUFFIX]; } if (allow) { if (classes[prop].length) { classes[prop] += ' '; } classes[prop] += klass; } }); function splitClassesToLookup(classes) { if (isString(classes)) { classes = classes.split(' '); } var obj = {}; forEach(classes, function(klass) { // sometimes the split leaves empty string values // incase extra spaces were applied to the options if (klass.length) { obj[klass] = true; } }); return obj; } return classes; } function getDomNode(element) { return (element instanceof jqLite) ? element[0] : element; } function applyGeneratedPreparationClasses(element, event, options) { var classes = ''; if (event) { classes = pendClasses(event, EVENT_CLASS_PREFIX, true); } if (options.addClass) { classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX)); } if (options.removeClass) { classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX)); } if (classes.length) { options.preparationClasses = classes; element.addClass(classes); } } function clearGeneratedClasses(element, options) { if (options.preparationClasses) { element.removeClass(options.preparationClasses); options.preparationClasses = null; } if (options.activeClasses) { element.removeClass(options.activeClasses); options.activeClasses = null; } } function blockTransitions(node, duration) { // we use a negative delay value since it performs blocking // yet it doesn't kill any existing transitions running on the // same element which makes this safe for class-based animations var value = duration ? '-' + duration + 's' : ''; applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]); return [TRANSITION_DELAY_PROP, value]; } function blockKeyframeAnimations(node, applyBlock) { var value = applyBlock ? 'paused' : ''; var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; applyInlineStyle(node, [key, value]); return [key, value]; } function applyInlineStyle(node, styleTuple) { var prop = styleTuple[0]; var value = styleTuple[1]; node.style[prop] = value; } function concatWithSpace(a,b) { if (!a) return b; if (!b) return a; return a + ' ' + b; } var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { var queue, cancelFn; function scheduler(tasks) { // we make a copy since RAFScheduler mutates the state // of the passed in array variable and this would be difficult // to track down on the outside code queue = queue.concat(tasks); nextTick(); } queue = scheduler.queue = []; /* waitUntilQuiet does two things: * 1. It will run the FINAL `fn` value only when an uncanceled RAF has passed through * 2. It will delay the next wave of tasks from running until the quiet `fn` has run. * * The motivation here is that animation code can request more time from the scheduler * before the next wave runs. This allows for certain DOM properties such as classes to * be resolved in time for the next animation to run. */ scheduler.waitUntilQuiet = function(fn) { if (cancelFn) cancelFn(); cancelFn = $$rAF(function() { cancelFn = null; fn(); nextTick(); }); }; return scheduler; function nextTick() { if (!queue.length) return; var items = queue.shift(); for (var i = 0; i < items.length; i++) { items[i](); } if (!cancelFn) { $$rAF(function() { if (!cancelFn) nextTick(); }); } } }]; /** * @ngdoc directive * @name ngAnimateChildren * @restrict AE * @element ANY * * @description * * ngAnimateChildren allows you to specify that children of this element should animate even if any * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` * (structural) animation, child elements that also have an active structural animation are not animated. * * Note that even if `ngAnimateChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). * * * @param {string} ngAnimateChildren If the value is empty, `true` or `on`, * then child animations are allowed. If the value is `false`, child animations are not allowed. * * @example * <example module="ngAnimateChildren" name="ngAnimateChildren" deps="angular-animate.js" animations="true"> <file name="index.html"> <div ng-controller="MainController as main"> <label>Show container? <input type="checkbox" ng-model="main.enterElement" /></label> <label>Animate children? <input type="checkbox" ng-model="main.animateChildren" /></label> <hr> <div ng-animate-children="{{main.animateChildren}}"> <div ng-if="main.enterElement" class="container"> List of items: <div ng-repeat="item in [0, 1, 2, 3]" class="item">Item {{item}}</div> </div> </div> </div> </file> <file name="animations.css"> .container.ng-enter, .container.ng-leave { transition: all ease 1.5s; } .container.ng-enter, .container.ng-leave-active { opacity: 0; } .container.ng-leave, .container.ng-enter-active { opacity: 1; } .item { background: firebrick; color: #FFF; margin-bottom: 10px; } .item.ng-enter, .item.ng-leave { transition: transform 1.5s ease; } .item.ng-enter { transform: translateX(50px); } .item.ng-enter-active { transform: translateX(0); } </file> <file name="script.js"> angular.module('ngAnimateChildren', ['ngAnimate']) .controller('MainController', function MainController() { this.animateChildren = false; this.enterElement = false; }); </file> </example> */ var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { return { link: function(scope, element, attrs) { var val = attrs.ngAnimateChildren; if (isString(val) && val.length === 0) { //empty attribute element.data(NG_ANIMATE_CHILDREN_DATA, true); } else { // Interpolate and set the value, so that it is available to // animations that run right after compilation setData($interpolate(val)(scope)); attrs.$observe('ngAnimateChildren', setData); } function setData(value) { value = value === 'on' || value === 'true'; element.data(NG_ANIMATE_CHILDREN_DATA, value); } } }; }]; /* exported $AnimateCssProvider */ var ANIMATE_TIMER_KEY = '$$animateCss'; /** * @ngdoc service * @name $animateCss * @kind object * * @description * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or * directives to create more complex animations that can be purely driven using CSS code. * * Note that only browsers that support CSS transitions and/or keyframe animations are capable of * rendering animations triggered via `$animateCss` (bad news for IE9 and lower). * * ## General Use * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however, * any automatic control over cancelling animations and/or preventing animations from being run on * child elements will not be handled by AngularJS. For this to work as expected, please use `$animate` to * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger * the CSS animation. * * The example below shows how we can create a folding animation on an element using `ng-if`: * * ```html * <!-- notice the `fold-animation` CSS class --> * <div ng-if="onOff" class="fold-animation"> * This element will go BOOM * </div> * <button ng-click="onOff=true">Fold In</button> * ``` * * Now we create the **JavaScript animation** that will trigger the CSS transition: * * ```js * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { * return { * enter: function(element, doneFn) { * var height = element[0].offsetHeight; * return $animateCss(element, { * from: { height:'0px' }, * to: { height:height + 'px' }, * duration: 1 // one second * }); * } * } * }]); * ``` * * ## More Advanced Uses * * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code. * * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation, * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order * to provide a working animation that will run in CSS. * * The example below showcases a more advanced version of the `.fold-animation` from the example above: * * ```js * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { * return { * enter: function(element, doneFn) { * var height = element[0].offsetHeight; * return $animateCss(element, { * addClass: 'red large-text pulse-twice', * easing: 'ease-out', * from: { height:'0px' }, * to: { height:height + 'px' }, * duration: 1 // one second * }); * } * } * }]); * ``` * * Since we're adding/removing CSS classes then the CSS transition will also pick those up: * * ```css * /&#42; since a hardcoded duration value of 1 was provided in the JavaScript animation code, * the CSS classes below will be transitioned despite them being defined as regular CSS classes &#42;/ * .red { background:red; } * .large-text { font-size:20px; } * * /&#42; we can also use a keyframe animation and $animateCss will make it work alongside the transition &#42;/ * .pulse-twice { * animation: 0.5s pulse linear 2; * -webkit-animation: 0.5s pulse linear 2; * } * * @keyframes pulse { * from { transform: scale(0.5); } * to { transform: scale(1.5); } * } * * @-webkit-keyframes pulse { * from { -webkit-transform: scale(0.5); } * to { -webkit-transform: scale(1.5); } * } * ``` * * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen. * * ## How the Options are handled * * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline * styles using the `from` and `to` properties. * * ```js * var animator = $animateCss(element, { * from: { background:'red' }, * to: { background:'blue' } * }); * animator.start(); * ``` * * ```css * .rotating-animation { * animation:0.5s rotate linear; * -webkit-animation:0.5s rotate linear; * } * * @keyframes rotate { * from { transform: rotate(0deg); } * to { transform: rotate(360deg); } * } * * @-webkit-keyframes rotate { * from { -webkit-transform: rotate(0deg); } * to { -webkit-transform: rotate(360deg); } * } * ``` * * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied * and spread across the transition and keyframe animation. * * ## What is returned * * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties: * * ```js * var animator = $animateCss(element, { ... }); * ``` * * Now what do the contents of our `animator` variable look like: * * ```js * { * // starts the animation * start: Function, * * // ends (aborts) the animation * end: Function * } * ``` * * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties * and that changing them will not reconfigure the parameters of the animation. * * ### runner.done() vs runner.then() * 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 * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**. * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()` * unless you really need a digest to kick off afterwards. * * 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 * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code). * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works. * * @param {DOMElement} element the element that will be animated * @param {object} options the animation-related options that will be applied during the animation * * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation. * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation. * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0` * is provided then the animation will be skipped entirely. * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same * CSS delay value. * * `stagger` - A numeric time value representing the delay between successively animated elements * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) * * `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 occurring on the classes being added and removed.) * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once * the animation is closed. This is useful for when the styles are used purely for the sake of * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation). * By default this value is set to `false`. * * @return {object} an object with start and end methods and details about the animation. * * * `start` - The method to start the animation. This will return a `Promise` when called. * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. */ var ONE_SECOND = 1000; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; var DETECT_CSS_PROPERTIES = { transitionDuration: TRANSITION_DURATION_PROP, transitionDelay: TRANSITION_DELAY_PROP, transitionProperty: TRANSITION_PROP + PROPERTY_KEY, animationDuration: ANIMATION_DURATION_PROP, animationDelay: ANIMATION_DELAY_PROP, animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY }; var DETECT_STAGGER_CSS_PROPERTIES = { transitionDuration: TRANSITION_DURATION_PROP, transitionDelay: TRANSITION_DELAY_PROP, animationDuration: ANIMATION_DURATION_PROP, animationDelay: ANIMATION_DELAY_PROP }; function getCssKeyframeDurationStyle(duration) { return [ANIMATION_DURATION_PROP, duration + 's']; } function getCssDelayStyle(delay, isKeyframeAnimation) { var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; return [prop, delay + 's']; } function computeCssStyles($window, element, properties) { var styles = Object.create(null); var detectedStyles = $window.getComputedStyle(element) || {}; forEach(properties, function(formalStyleName, actualStyleName) { var val = detectedStyles[formalStyleName]; if (val) { var c = val.charAt(0); // only numerical-based values have a negative sign or digit as the first value if (c === '-' || c === '+' || c >= 0) { val = parseMaxTime(val); } // by setting this to null in the event that the delay is not set or is set directly as 0 // then we can still allow for negative values to be used later on and not mistake this // value for being greater than any other negative value. if (val === 0) { val = null; } styles[actualStyleName] = val; } }); return styles; } function parseMaxTime(str) { var maxValue = 0; var values = str.split(/\s*,\s*/); forEach(values, function(value) { // it's always safe to consider only second values and omit `ms` values since // getComputedStyle will always handle the conversion for us if (value.charAt(value.length - 1) === 's') { value = value.substring(0, value.length - 1); } value = parseFloat(value) || 0; maxValue = maxValue ? Math.max(value, maxValue) : value; }); return maxValue; } function truthyTimingValue(val) { return val === 0 || val != null; } function getCssTransitionDurationStyle(duration, applyOnlyDuration) { var style = TRANSITION_PROP; var value = duration + 's'; if (applyOnlyDuration) { style += DURATION_KEY; } else { value += ' linear all'; } return [style, value]; } function createLocalCacheLookup() { var cache = Object.create(null); return { flush: function() { cache = Object.create(null); }, count: function(key) { var entry = cache[key]; return entry ? entry.total : 0; }, get: function(key) { var entry = cache[key]; return entry && entry.value; }, put: function(key, value) { if (!cache[key]) { cache[key] = { total: 1, value: value }; } else { cache[key].total++; } } }; } // we do not reassign an already present style value since // if we detect the style property value again we may be // detecting styles that were added via the `from` styles. // We make use of `isDefined` here since an empty string // or null value (which is what getPropertyValue will return // for a non-existing style) will still be marked as a valid // value for the style (a falsy value implies that the style // is to be removed at the end of the animation). If we had a simple // "OR" statement then it would not be enough to catch that. function registerRestorableStyles(backup, node, properties) { forEach(properties, function(prop) { backup[prop] = isDefined(backup[prop]) ? backup[prop] : node.style.getPropertyValue(prop); }); } var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animateProvider) { var gcsLookup = createLocalCacheLookup(); var gcsStaggerLookup = createLocalCacheLookup(); this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', function($window, $$jqLite, $$AnimateRunner, $timeout, $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); var parentCounter = 0; function gcsHashFn(node, extraClasses) { var KEY = '$$ngAnimateParentKey'; var parentNode = node.parentNode; var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; } function computeCachedCssStyles(node, className, cacheKey, properties) { var timings = gcsLookup.get(cacheKey); if (!timings) { timings = computeCssStyles($window, node, properties); if (timings.animationIterationCount === 'infinite') { timings.animationIterationCount = 1; } } // we keep putting this in multiple times even though the value and the cacheKey are the same // because we're keeping an internal tally of how many duplicate animations are detected. gcsLookup.put(cacheKey, timings); return timings; } function computeCachedCssStaggerStyles(node, className, cacheKey, properties) { var stagger; // if we have one or more existing matches of matching elements // containing the same parent + CSS styles (which is how cacheKey works) // then staggering is possible if (gcsLookup.count(cacheKey) > 0) { stagger = gcsStaggerLookup.get(cacheKey); if (!stagger) { var staggerClassName = pendClasses(className, '-stagger'); $$jqLite.addClass(node, staggerClassName); stagger = computeCssStyles($window, node, properties); // force the conversion of a null value to zero incase not set stagger.animationDuration = Math.max(stagger.animationDuration, 0); stagger.transitionDuration = Math.max(stagger.transitionDuration, 0); $$jqLite.removeClass(node, staggerClassName); gcsStaggerLookup.put(cacheKey, stagger); } } return stagger || {}; } var rafWaitQueue = []; function waitUntilQuiet(callback) { rafWaitQueue.push(callback); $$rAFScheduler.waitUntilQuiet(function() { gcsLookup.flush(); gcsStaggerLookup.flush(); // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. // PLEASE EXAMINE THE `$$forceReflow` service to understand why. var pageWidth = $$forceReflow(); // we use a for loop to ensure that if the queue is changed // during this looping then it will consider new requests for (var i = 0; i < rafWaitQueue.length; i++) { rafWaitQueue[i](pageWidth); } rafWaitQueue.length = 0; }); } function computeTimings(node, className, cacheKey) { var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); var aD = timings.animationDelay; var tD = timings.transitionDelay; timings.maxDelay = aD && tD ? Math.max(aD, tD) : (aD || tD); timings.maxDuration = Math.max( timings.animationDuration * timings.animationIterationCount, timings.transitionDuration); return timings; } return function init(element, initialOptions) { // all of the animation functions should create // a copy of the options data, however, if a // parent service has already created a copy then // we should stick to using that var options = initialOptions || {}; if (!options.$$prepared) { options = prepareAnimationOptions(copy(options)); } var restoreStyles = {}; var node = getDomNode(element); if (!node || !node.parentNode || !$$animateQueue.enabled()) { return closeAndReturnNoopAnimator(); } var temporaryStyles = []; var classes = element.attr('class'); var styles = packageStyles(options); var animationClosed; var animationPaused; var animationCompleted; var runner; var runnerHost; var maxDelay; var maxDelayTime; var maxDuration; var maxDurationTime; var startTime; var events = []; if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { return closeAndReturnNoopAnimator(); } var method = options.event && isArray(options.event) ? options.event.join(' ') : options.event; var isStructural = method && options.structural; var structuralClassName = ''; var addRemoveClassName = ''; if (isStructural) { structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); } else if (method) { structuralClassName = method; } if (options.addClass) { addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); } if (options.removeClass) { if (addRemoveClassName.length) { addRemoveClassName += ' '; } addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); } // there may be a situation where a structural animation is combined together // with CSS classes that need to resolve before the animation is computed. // However this means that there is no explicit CSS code to block the animation // from happening (by setting 0s none in the class name). If this is the case // we need to apply the classes before the first rAF so we know to continue if // there actually is a detected transition or keyframe animation if (options.applyClassesEarly && addRemoveClassName.length) { applyAnimationClasses(element, options); } var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); var fullClassName = classes + ' ' + preparationClasses; var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); var hasToStyles = styles.to && Object.keys(styles.to).length > 0; var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; // there is no way we can trigger an animation if no styles and // no classes are being applied which would then trigger a transition, // unless there a is raw keyframe value that is applied to the element. if (!containsKeyframeAnimation && !hasToStyles && !preparationClasses) { return closeAndReturnNoopAnimator(); } var cacheKey, stagger; if (options.stagger > 0) { var staggerVal = parseFloat(options.stagger); stagger = { transitionDelay: staggerVal, animationDelay: staggerVal, transitionDuration: 0, animationDuration: 0 }; } else { cacheKey = gcsHashFn(node, fullClassName); stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); } if (!options.$$skipPreparationClasses) { $$jqLite.addClass(element, preparationClasses); } var applyOnlyDuration; if (options.transitionStyle) { var transitionStyle = [TRANSITION_PROP, options.transitionStyle]; applyInlineStyle(node, transitionStyle); temporaryStyles.push(transitionStyle); } if (options.duration >= 0) { applyOnlyDuration = node.style[TRANSITION_PROP].length > 0; var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration); // we set the duration so that it will be picked up by getComputedStyle later applyInlineStyle(node, durationStyle); temporaryStyles.push(durationStyle); } if (options.keyframeStyle) { var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle]; applyInlineStyle(node, keyframeStyle); temporaryStyles.push(keyframeStyle); } var itemIndex = stagger ? options.staggerIndex >= 0 ? options.staggerIndex : gcsLookup.count(cacheKey) : 0; var isFirst = itemIndex === 0; // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY // without causing any combination of transitions to kick in. By adding a negative delay value // it forces the setup class' transition to end immediately. We later then remove the negative // transition delay to allow for the transition to naturally do it's thing. The beauty here is // that if there is no transition defined then nothing will happen and this will also allow // other transitions to be stacked on top of each other without any chopping them out. if (isFirst && !options.skipBlocking) { blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); } var timings = computeTimings(node, fullClassName, cacheKey); var relativeDelay = timings.maxDelay; maxDelay = Math.max(relativeDelay, 0); maxDuration = timings.maxDuration; var flags = {}; flags.hasTransitions = timings.transitionDuration > 0; flags.hasAnimations = timings.animationDuration > 0; flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty === 'all'; flags.applyTransitionDuration = hasToStyles && ( (flags.hasTransitions && !flags.hasTransitionAll) || (flags.hasAnimations && !flags.hasTransitions)); flags.applyAnimationDuration = options.duration && flags.hasAnimations; flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions); flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations; flags.recalculateTimingStyles = addRemoveClassName.length > 0; if (flags.applyTransitionDuration || flags.applyAnimationDuration) { maxDuration = options.duration ? parseFloat(options.duration) : maxDuration; if (flags.applyTransitionDuration) { flags.hasTransitions = true; timings.transitionDuration = maxDuration; applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0; temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration)); } if (flags.applyAnimationDuration) { flags.hasAnimations = true; timings.animationDuration = maxDuration; temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration)); } } if (maxDuration === 0 && !flags.recalculateTimingStyles) { return closeAndReturnNoopAnimator(); } if (options.delay != null) { var delayStyle; if (typeof options.delay !== 'boolean') { delayStyle = parseFloat(options.delay); // number in options.delay means we have to recalculate the delay for the closing timeout maxDelay = Math.max(delayStyle, 0); } if (flags.applyTransitionDelay) { temporaryStyles.push(getCssDelayStyle(delayStyle)); } if (flags.applyAnimationDelay) { temporaryStyles.push(getCssDelayStyle(delayStyle, true)); } } // we need to recalculate the delay value since we used a pre-emptive negative // delay value and the delay value is required for the final event checking. This // property will ensure that this will happen after the RAF phase has passed. if (options.duration == null && timings.transitionDuration > 0) { flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst; } maxDelayTime = maxDelay * ONE_SECOND; maxDurationTime = maxDuration * ONE_SECOND; if (!options.skipBlocking) { flags.blockTransition = timings.transitionDuration > 0; flags.blockKeyframeAnimation = timings.animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0; } if (options.from) { if (options.cleanupStyles) { registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); } applyAnimationFromStyles(element, options); } if (flags.blockTransition || flags.blockKeyframeAnimation) { applyBlocking(maxDuration); } else if (!options.skipBlocking) { blockTransitions(node, false); } // TODO(matsko): for 1.5 change this code to have an animator object for better debugging return { $$willAnimate: true, end: endFn, start: function() { if (animationClosed) return; runnerHost = { end: endFn, cancel: cancelFn, resume: null, //this will be set during the start() phase pause: null }; runner = new $$AnimateRunner(runnerHost); waitUntilQuiet(start); // we don't have access to pause/resume the animation // since it hasn't run yet. AnimateRunner will therefore // set noop functions for resume and pause and they will // later be overridden once the animation is triggered return runner; } }; function endFn() { close(); } function cancelFn() { close(true); } function close(rejected) { // if the promise has been called already then we shouldn't close // the animation again if (animationClosed || (animationCompleted && animationPaused)) return; animationClosed = true; animationPaused = false; if (!options.$$skipPreparationClasses) { $$jqLite.removeClass(element, preparationClasses); } $$jqLite.removeClass(element, activeClasses); blockKeyframeAnimations(node, false); blockTransitions(node, false); forEach(temporaryStyles, function(entry) { // There is only one way to remove inline style properties entirely from elements. // By using `removeProperty` this works, but we need to convert camel-cased CSS // styles down to hyphenated values. node.style[entry[0]] = ''; }); applyAnimationClasses(element, options); applyAnimationStyles(element, options); if (Object.keys(restoreStyles).length) { forEach(restoreStyles, function(value, prop) { if (value) { node.style.setProperty(prop, value); } else { node.style.removeProperty(prop); } }); } // the reason why we have this option is to allow a synchronous closing callback // that is fired as SOON as the animation ends (when the CSS is removed) or if // the animation never takes off at all. A good example is a leave animation since // the element must be removed just after the animation is over or else the element // will appear on screen for one animation frame causing an overbearing flicker. if (options.onDone) { options.onDone(); } if (events && events.length) { // Remove the transitionend / animationend listener(s) element.off(events.join(' '), onAnimationProgress); } //Cancel the fallback closing timeout and remove the timer data var animationTimerData = element.data(ANIMATE_TIMER_KEY); if (animationTimerData) { $timeout.cancel(animationTimerData[0].timer); element.removeData(ANIMATE_TIMER_KEY); } // if the preparation function fails then the promise is not setup if (runner) { runner.complete(!rejected); } } function applyBlocking(duration) { if (flags.blockTransition) { blockTransitions(node, duration); } if (flags.blockKeyframeAnimation) { blockKeyframeAnimations(node, !!duration); } } function closeAndReturnNoopAnimator() { runner = new $$AnimateRunner({ end: endFn, cancel: cancelFn }); // should flush the cache animation waitUntilQuiet(noop); close(); return { $$willAnimate: false, start: function() { return runner; }, end: endFn }; } function onAnimationProgress(event) { event.stopPropagation(); var ev = event.originalEvent || event; if (ev.target !== node) { // Since TransitionEvent / AnimationEvent bubble up, // we have to ignore events by finished child animations return; } // we now always use `Date.now()` due to the recent changes with // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info) var timeStamp = ev.$manualTimeStamp || Date.now(); /* Firefox (or possibly just Gecko) likes to not round values up * when a ms measurement is used for the animation */ var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); /* $manualTimeStamp is a mocked timeStamp value which is set * within browserTrigger(). This is only here so that tests can * mock animations properly. Real events fallback to event.timeStamp, * or, if they don't, then a timeStamp is automatically created for them. * We're checking to see if the timeStamp surpasses the expected delay, * but we're using elapsedTime instead of the timeStamp on the 2nd * pre-condition since animationPauseds sometimes close off early */ if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { // we set this flag to ensure that if the transition is paused then, when resumed, // the animation will automatically close itself since transitions cannot be paused. animationCompleted = true; close(); } } function start() { if (animationClosed) return; if (!node.parentNode) { close(); return; } // even though we only pause keyframe animations here the pause flag // will still happen when transitions are used. Only the transition will // not be paused since that is not possible. If the animation ends when // paused then it will not complete until unpaused or cancelled. var playPause = function(playAnimation) { if (!animationCompleted) { animationPaused = !playAnimation; if (timings.animationDuration) { var value = blockKeyframeAnimations(node, animationPaused); if (animationPaused) { temporaryStyles.push(value); } else { remov