'use strict'; var $animateMinErr = minErr('$animate'); var ELEMENT_NODE = 1; var NG_ANIMATE_CLASSNAME = 'ng-animate'; function mergeClasses(a,b) { if (!a && !b) return ''; if (!a) return b; if (!b) return a; if (isArray(a)) a = a.join(' '); if (isArray(b)) b = b.join(' '); return a + ' ' + b; } function extractElementNode(element) { for (var i = 0; i < element.length; i++) { var elm = element[i]; if (elm.nodeType === ELEMENT_NODE) { return elm; } } } function splitClasses(classes) { if (isString(classes)) { classes = classes.split(' '); } // Use createMap() to prevent class assumptions involving property names in // Object.prototype var obj = createMap(); 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; } // if any other type of options value besides an Object value is // passed into the $animate.method() animation then this helper code // will be run which will ignore it. While this patch is not the // greatest solution to this, a lot of existing plugins depend on // $animate to either call the callback (< 1.2) or return a promise // that can be changed. This helper function ensures that the options // are wiped clean incase a callback function is provided. function prepareAnimateOptions(options) { return isObject(options) ? options : {}; } var $$CoreAnimateJsProvider = /** @this */ function() { this.$get = noop; }; // this is prefixed with Core since it conflicts with // the animateQueueProvider defined in ngAnimate/animateQueue.js var $$CoreAnimateQueueProvider = /** @this */ function() { var postDigestQueue = new HashMap(); var postDigestElements = []; this.$get = ['$$AnimateRunner', '$rootScope', function($$AnimateRunner, $rootScope) { return { enabled: noop, on: noop, off: noop, pin: noop, push: function(element, event, options, domOperation) { if (domOperation) { domOperation(); } options = options || {}; if (options.from) { element.css(options.from); } if (options.to) { element.css(options.to); } if (options.addClass || options.removeClass) { addRemoveClassesPostDigest(element, options.addClass, options.removeClass); } var runner = new $$AnimateRunner(); // since there are no animations to run the runner needs to be // notified that the animation call is complete. runner.complete(); return runner; } }; function updateData(data, classes, value) { var changed = false; if (classes) { classes = isString(classes) ? classes.split(' ') : isArray(classes) ? classes : []; forEach(classes, function(className) { if (className) { changed = true; data[className] = value; } }); } return changed; } function handleCSSClassChanges() { forEach(postDigestElements, function(element) { var data = postDigestQueue.get(element); if (data) { var existing = splitClasses(element.attr('class')); var toAdd = ''; var toRemove = ''; forEach(data, function(status, className) { var hasClass = !!existing[className]; if (status !== hasClass) { if (status) { toAdd += (toAdd.length ? ' ' : '') + className; } else { toRemove += (toRemove.length ? ' ' : '') + className; } } }); forEach(element, function(elm) { if (toAdd) { jqLiteAddClass(elm, toAdd); } if (toRemove) { jqLiteRemoveClass(elm, toRemove); } }); postDigestQueue.remove(element); } }); postDigestElements.length = 0; } function addRemoveClassesPostDigest(element, add, remove) { var data = postDigestQueue.get(element) || {}; var classesAdded = updateData(data, add, true); var classesRemoved = updateData(data, remove, false); if (classesAdded || classesRemoved) { postDigestQueue.put(element, data); postDigestElements.push(element); if (postDigestElements.length === 1) { $rootScope.$$postDigest(handleCSSClassChanges); } } } }]; }; /** * @ngdoc provider * @name $animateProvider * * @description * Default implementation of $animate that doesn't perform any animations, instead just * synchronously performs DOM updates and resolves the returned runner promise. * * In order to enable animations the `ngAnimate` module has to be loaded. * * To see the functional implementation check out `src/ngAnimate/animate.js`. */ var $AnimateProvider = ['$provide', /** @this */ function($provide) { var provider = this; this.$$registeredAnimations = Object.create(null); /** * @ngdoc method * @name $animateProvider#register * * @description * Registers a new injectable animation factory function. The factory function produces the * animation object which contains callback functions for each event that is expected to be * animated. * * * `eventFn`: `function(element, ... , doneFunction, options)` * The element to animate, the `doneFunction` and the options fed into the animation. Depending * on the type of animation additional arguments will be injected into the animation function. The * list below explains the function signatures for the different animation methods: * * - setClass: function(element, addedClasses, removedClasses, doneFunction, options) * - addClass: function(element, addedClasses, doneFunction, options) * - removeClass: function(element, removedClasses, doneFunction, options) * - enter, leave, move: function(element, doneFunction, options) * - animate: function(element, fromStyles, toStyles, doneFunction, options) * * Make sure to trigger the `doneFunction` once the animation is fully complete. * * ```js * return { * //enter, leave, move signature * eventFn : function(element, done, options) { * //code to run the animation * //once complete, then run done() * return function endFunction(wasCancelled) { * //code to cancel the animation * } * } * } * ``` * * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to). * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { if (name && name.charAt(0) !== '.') { throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name); } var key = name + '-animation'; provider.$$registeredAnimations[name.substr(1)] = key; $provide.factory(key, factory); }; /** * @ngdoc method * @name $animateProvider#classNameFilter * * @description * Sets and/or returns the CSS class regular expression that is checked when performing * an animation. Upon bootstrap the classNameFilter value is not set at all and will * therefore enable $animate to attempt to perform an animation on any element that is triggered. * When setting the `classNameFilter` value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. * @param {RegExp=} expression The className expression which will be checked against all animations * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { if (arguments.length === 1) { this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; if (this.$$classNameFilter) { var reservedRegex = new RegExp('(\\s+|\\/)' + NG_ANIMATE_CLASSNAME + '(\\s+|\\/)'); if (reservedRegex.test(this.$$classNameFilter.toString())) { throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); } } } return this.$$classNameFilter; }; this.$get = ['$$animateQueue', function($$animateQueue) { function domInsert(element, parentElement, afterElement) { // if for some reason the previous element was removed // from the dom sometime before this code runs then let's // just stick to using the parent element as the anchor if (afterElement) { var afterNode = extractElementNode(afterElement); if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) { afterElement = null; } } if (afterElement) { afterElement.after(element); } else { parentElement.prepend(element); } } /** * @ngdoc service * @name $animate * @description The $animate service exposes a series of DOM utility methods that provide support * for animation hooks. The default behavior is the application of DOM operations, however, * when an animation is detected (and animations are enabled), $animate will do the heavy lifting * to ensure that animation runs with the triggered DOM operation. * * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't * included and only when it is active then the animation hooks that `$animate` triggers will be * functional. Once active then all structural `ng-` directives will trigger animations as they perform * their DOM-related operations (enter, leave and move). Other directives such as `ngClass`, * `ngShow`, `ngHide` and `ngMessages` also provide support for animations. * * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives. * * To learn more about enabling animation support, click here to visit the * {@link ngAnimate ngAnimate module page}. */ return { // we don't call it directly since non-existant arguments may // be interpreted as null within the sub enabled function /** * * @ngdoc method * @name $animate#on * @kind function * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...) * has fired on the given element or among any of its children. Once the listener is fired, the provided callback * is fired with the following params: * * ```js * $animate.on('enter', container, * function callback(element, phase) { * // cool we detected an enter animation within the container * } * ); * ``` * * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...) * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself * as well as among its children * @param {Function} callback the callback function that will be fired when the listener is triggered * * The arguments present in the callback function are: * * `element` - The captured DOM element that the animation was fired on. * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends). */ on: $$animateQueue.on, /** * * @ngdoc method * @name $animate#off * @kind function * @description Deregisters an event listener based on the event which has been associated with the provided element. This method * can be used in three different ways depending on the arguments: * * ```js * // remove all the animation event listeners listening for `enter` * $animate.off('enter'); * * // remove listeners for all animation events from the container element * $animate.off(container); * * // remove all the animation event listeners listening for `enter` on the given element and its children * $animate.off('enter', container); * * // remove the event listener function provided by `callback` that is set * // to listen for `enter` on the given `container` as well as its children * $animate.off('enter', container, callback); * ``` * * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move, * addClass, removeClass, etc...), or the container element. If it is the element, all other * arguments are ignored. * @param {DOMElement=} container the container element the event listener was placed on * @param {Function=} callback the callback function that was registered as the listener */ off: $$animateQueue.off, /** * @ngdoc method * @name $animate#pin * @kind function * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the * element despite being outside the realm of the application or within another application. Say for example if the application * was bootstrapped on an element that is somewhere inside of the `` tag, but we wanted to allow for an element to be situated * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association. * * Note that this feature is only active when the `ngAnimate` module is used. * * @param {DOMElement} element the external element that will be pinned * @param {DOMElement} parentElement the host parent element that will be associated with the external element */ pin: $$animateQueue.pin, /** * * @ngdoc method * @name $animate#enabled * @kind function * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This * function can be called in four ways: * * ```js * // returns true or false * $animate.enabled(); * * // changes the enabled state for all animations * $animate.enabled(false); * $animate.enabled(true); * * // returns true or false if animations are enabled for an element * $animate.enabled(element); * * // changes the enabled state for an element and its children * $animate.enabled(element, true); * $animate.enabled(element, false); * ``` * * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state * @param {boolean=} enabled whether or not the animations will be enabled for the element * * @return {boolean} whether or not animations are enabled */ enabled: $$animateQueue.enabled, /** * @ngdoc method * @name $animate#cancel * @kind function * @description Cancels the provided animation. * * @param {Promise} animationPromise The animation promise that is returned when an animation is started. */ cancel: function(runner) { if (runner.end) { runner.end(); } }, /** * * @ngdoc method * @name $animate#enter * @kind function * @description Inserts the element into the DOM either after the `after` element (if provided) or * as the first child within the `parent` element and then triggers an animation. * A promise is returned that will be resolved during the next digest once the animation * has completed. * * @param {DOMElement} element the element which will be inserted into the DOM * @param {DOMElement} parent the parent element which will append the element as * a child (so long as the after element is not present) * @param {DOMElement=} after the sibling element after which the element will be appended * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ enter: function(element, parent, after, options) { parent = parent && jqLite(parent); after = after && jqLite(after); parent = parent || after.parent(); domInsert(element, parent, after); return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options)); }, /** * * @ngdoc method * @name $animate#move * @kind function * @description Inserts (moves) the element into its new position in the DOM either after * the `after` element (if provided) or as the first child within the `parent` element * and then triggers an animation. A promise is returned that will be resolved * during the next digest once the animation has completed. * * @param {DOMElement} element the element which will be moved into the new DOM position * @param {DOMElement} parent the parent element which will append the element as * a child (so long as the after element is not present) * @param {DOMElement=} after the sibling element after which the element will be appended * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ move: function(element, parent, after, options) { parent = parent && jqLite(parent); after = after && jqLite(after); parent = parent || after.parent(); domInsert(element, parent, after); return $$animateQueue.push(element, 'move', prepareAnimateOptions(options)); }, /** * @ngdoc method * @name $animate#leave * @kind function * @description Triggers an animation and then removes the element from the DOM. * When the function is called a promise is returned that will be resolved during the next * digest once the animation has completed. * * @param {DOMElement} element the element which will be removed from the DOM * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ leave: function(element, options) { return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { element.remove(); }); }, /** * @ngdoc method * @name $animate#addClass * @kind function * * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon * execution, the addClass operation will only be handled after the next digest and it will not trigger an * animation if element already contains the CSS class or if the class is removed at a later step. * Note that class-based animations are treated differently compared to structural animations * (like enter, move and leave) since the CSS classes may be added/removed at different points * depending if CSS or JavaScript animations are used. * * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ addClass: function(element, className, options) { options = prepareAnimateOptions(options); options.addClass = mergeClasses(options.addclass, className); return $$animateQueue.push(element, 'addClass', options); }, /** * @ngdoc method * @name $animate#removeClass * @kind function * * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon * execution, the removeClass operation will only be handled after the next digest and it will not trigger an * animation if element does not contain the CSS class or if the class is added at a later step. * Note that class-based animations are treated differently compared to structural animations * (like enter, move and leave) since the CSS classes may be added/removed at different points * depending if CSS or JavaScript animations are used. * * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ removeClass: function(element, className, options) { options = prepareAnimateOptions(options); options.removeClass = mergeClasses(options.removeClass, className); return $$animateQueue.push(element, 'removeClass', options); }, /** * @ngdoc method * @name $animate#setClass * @kind function * * @description Performs both the addition and removal of a CSS classes on an element and (during the process) * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has * passed. Note that class-based animations are treated differently compared to structural animations * (like enter, move and leave) since the CSS classes may be added/removed at different points * depending if CSS or JavaScript animations are used. * * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ setClass: function(element, add, remove, options) { options = prepareAnimateOptions(options); options.addClass = mergeClasses(options.addClass, add); options.removeClass = mergeClasses(options.removeClass, remove); return $$animateQueue.push(element, 'setClass', options); }, /** * @ngdoc method * @name $animate#animate * @kind function * * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding * style in `to`, the style in `from` is applied immediately, and no animation is run. * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate` * method (or as part of the `options` parameter): * * ```js * ngModule.animation('.my-inline-animation', function() { * return { * animate : function(element, from, to, done, options) { * //animation * done(); * } * } * }); * ``` * * @param {DOMElement} element the element which the CSS styles will be applied to * @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation. * @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation. * @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If * this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element. * (Note that if no animation is detected then this value will not be applied to the element.) * @param {object=} options an optional collection of options/styles that will be applied to the element. * The object can have the following properties: * * - **addClass** - `{string}` - space-separated CSS classes to add to element * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` * - **removeClass** - `{string}` - space-separated CSS classes to remove from element * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ animate: function(element, from, to, className, options) { options = prepareAnimateOptions(options); options.from = options.from ? extend(options.from, from) : from; options.to = options.to ? extend(options.to, to) : to; className = className || 'ng-inline-animate'; options.tempClasses = mergeClasses(options.tempClasses, className); return $$animateQueue.push(element, 'animate', options); } }; }]; }];