diff options
author | Rob Cresswell <robert.cresswell@outlook.com> | 2016-04-20 11:27:00 +0100 |
---|---|---|
committer | Rob Cresswell <robert.cresswell@outlook.com> | 2016-04-20 11:27:35 +0100 |
commit | 65cde9c21257795353c9d111c232596de1bcb995 (patch) | |
tree | fb67910cb984770e441c35c6c0028883a4592667 /xstatic/pkg | |
parent | aee8cd393d9a8fe6f50211265712740f6d8568d4 (diff) | |
download | xstatic-angular-65cde9c21257795353c9d111c232596de1bcb995.tar.gz |
Update XStatic-Angular to 1.4.10
Change-Id: Ie61a6282f792a8721674014dc11f2e2a2ec1245b
Diffstat (limited to 'xstatic/pkg')
-rw-r--r-- | xstatic/pkg/angular/__init__.py | 4 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-animate.js | 5518 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-aria.js | 111 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-cookies.js | 403 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-csp.css | 8 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-loader.js | 113 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-message-format.js | 980 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-messages.js | 649 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-mocks.js | 377 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-route.js | 31 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-sanitize.js | 64 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-touch.js | 18 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular.js | 9955 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/errors.json | 2 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/version.json | 2 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/version.txt | 2 |
16 files changed, 12534 insertions, 5703 deletions
diff --git a/xstatic/pkg/angular/__init__.py b/xstatic/pkg/angular/__init__.py index 1eec0a1..c4bf407 100644 --- a/xstatic/pkg/angular/__init__.py +++ b/xstatic/pkg/angular/__init__.py @@ -11,9 +11,9 @@ NAME = __name__.split('.')[-1] # package name (e.g. 'foo' or 'foo_bar') # please use a all-lowercase valid python # package name -VERSION = '1.3.18' # version of the packaged files, please use the upstream +VERSION = '1.4.10' # version of the packaged files, please use the upstream # version number -BUILD = '1' # our package build number, so we can release new builds +BUILD = '0' # our package build number, so we can release new builds # with fixes for xstatic stuff. PACKAGE_VERSION = VERSION + '.' + BUILD # version used for PyPi diff --git a/xstatic/pkg/angular/data/angular-animate.js b/xstatic/pkg/angular/data/angular-animate.js index cd835d0..7c0677e 100644 --- a/xstatic/pkg/angular/data/angular-animate.js +++ b/xstatic/pkg/angular/data/angular-animate.js @@ -1,2142 +1,4010 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; -/* jshint maxlen: false */ +/* jshint ignore:start */ +var noop = angular.noop; +var copy = angular.copy; +var extend = angular.extend; +var jqLite = angular.element; +var forEach = angular.forEach; +var isArray = angular.isArray; +var isString = angular.isString; +var isObject = angular.isObject; +var isUndefined = angular.isUndefined; +var isDefined = angular.isDefined; +var isFunction = angular.isFunction; +var isElement = angular.isElement; + +var ELEMENT_NODE = 1; +var COMMENT_NODE = 8; + +var ADD_CLASS_SUFFIX = '-add'; +var REMOVE_CLASS_SUFFIX = '-remove'; +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 (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) { + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; +} else { + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; +} + +if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { + 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 isPromiseLike = function(p) { + return p && p.then ? true : false; +}; + +var ngMinErr = angular.$$minErr('ng'); +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + } + 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 []; + break; + + 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)); + break; + } + } + + 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]; + } else if (val === REMOVE_CLASS) { + prop = 'removeClass'; + allow = existing[klass]; + } + if (allow) { + if (classes[prop].length) { + classes[prop] += ' '; + } + classes[prop] += klass; + } + }); + + function splitClassesToLookup(classes) { + if (isString(classes)) { + classes = classes.split(' '); + } + + var obj = {}; + forEach(classes, function(klass) { + // sometimes the split leaves empty string values + // incase extra spaces were applied to the options + if (klass.length) { + obj[klass] = true; + } + }); + return obj; + } + + return classes; +} + +function getDomNode(element) { + return (element instanceof angular.element) ? 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 uncancelled 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 module - * @name ngAnimate - * @description - * - * The `ngAnimate` module provides support for JavaScript, CSS3 transition and CSS3 keyframe animation hooks within existing core and custom directives. - * - * <div doc-module-components="ngAnimate"></div> - * - * # Usage - * - * To see animations in action, all that is required is to define the appropriate CSS classes - * or to register a JavaScript animation via the `myModule.animation()` function. The directives that support animation automatically are: - * `ngRepeat`, `ngInclude`, `ngIf`, `ngSwitch`, `ngShow`, `ngHide`, `ngView` and `ngClass`. Custom directives can take advantage of animation - * by using the `$animate` service. - * - * Below is a more detailed breakdown of the supported animation events provided by pre-existing ng directives: - * - * | Directive | Supported Animations | - * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| - * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | - * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | - * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | - * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | - * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | - * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * @ngdoc directive + * @name ngAnimateChildren + * @restrict AE + * @element ANY * - * You can find out more information about animations upon visiting each directive page. + * @description * - * Below is an example of how to apply animations to a directive that supports animation hooks: + * ngAnimateChildren allows you to specify that children of this element should animate even if any + * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` + * (structural) animation, child elements that also have an active structural animation are not animated. + * + * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). + * + * + * @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() { + this.animateChildren = false; + this.enterElement = false; + }); + </file> + </example> + */ +var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { + return { + link: function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + 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); + } + } + }; +}]; + +var ANIMATE_TIMER_KEY = '$$animateCss'; + +/** + * @ngdoc service + * @name $animateCss + * @kind object * - * ```html - * <style type="text/css"> - * .slide.ng-enter, .slide.ng-leave { - * -webkit-transition:0.5s linear all; - * transition:0.5s linear all; - * } + * @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. * - * .slide.ng-enter { } /* starting animations for enter */ - * .slide.ng-enter.ng-enter-active { } /* terminal animations for enter */ - * .slide.ng-leave { } /* starting animations for leave */ - * .slide.ng-leave.ng-leave-active { } /* terminal animations for leave */ - * </style> + * 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). * - * <!-- - * the animate service will automatically add .ng-enter and .ng-leave to the element - * to trigger the CSS transition/animations - * --> - * <ANY class="slide" ng-include="..."></ANY> - * ``` + * ## Usage + * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that + * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however, + * any automatic control over cancelling animations and/or preventing animations from being run on + * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to + * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger + * the CSS animation. * - * Keep in mind that, by default, if an animation is running, any child elements cannot be animated - * until the parent element's animation has completed. This blocking feature can be overridden by - * placing the `ng-animate-children` attribute on a parent container tag. + * The example below shows how we can create a folding animation on an element using `ng-if`: * * ```html - * <div class="slide-animation" ng-if="on" ng-animate-children> - * <div class="fade-animation" ng-if="on"> - * <div class="explode-animation" ng-if="on"> - * ... - * </div> - * </div> + * <!-- 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> * ``` * - * When the `on` expression value changes and an animation is triggered then each of the elements within - * will all animate without the block being applied to child elements. + * Now we create the **JavaScript animation** that will trigger the CSS transition: * - * ## Are animations run when the application starts? - * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid - * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work, - * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering - * layout changes in the application will trigger animations as normal. - * - * In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular - * will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests - * are complete. - * - * ## CSS-defined Animations - * The animate service will automatically apply two CSS classes to the animated element and these two CSS classes - * are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported - * and can be used to play along with this naming structure. - * - * The following code below demonstrates how to perform animations using **CSS transitions** with Angular: - * - * ```html - * <style type="text/css"> - * /* - * The animate class is apart of the element and the ng-enter class - * is attached to the element once the enter animation event is triggered - * */ - * .reveal-animation.ng-enter { - * -webkit-transition: 1s linear all; /* Safari/Chrome */ - * transition: 1s linear all; /* All other modern browsers and IE10+ */ - * - * /* The animation preparation code */ - * opacity: 0; - * } + * ```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 + * }); + * } + * } + * }]); + * ``` * - * /* - * Keep in mind that you want to combine both CSS - * classes together to avoid any CSS-specificity - * conflicts - * */ - * .reveal-animation.ng-enter.ng-enter-active { - * /* The animation code itself */ - * opacity: 1; - * } - * </style> + * ## More Advanced Uses * - * <div class="view-container"> - * <div ng-view class="reveal-animation"></div> - * </div> - * ``` + * `$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. * - * The following code below demonstrates how to perform animations using **CSS animations** with Angular: + * 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. * - * ```html - * <style type="text/css"> - * .reveal-animation.ng-enter { - * -webkit-animation: enter_sequence 1s linear; /* Safari/Chrome */ - * animation: enter_sequence 1s linear; /* IE10+ and Future Browsers */ - * } - * @-webkit-keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * @keyframes enter_sequence { - * from { opacity:0; } - * to { opacity:1; } - * } - * </style> + * The example below showcases a more advanced version of the `.fold-animation` from the example above: * - * <div class="view-container"> - * <div ng-view class="reveal-animation"></div> - * </div> + * ```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 + * }); + * } + * } + * }]); * ``` * - * Both CSS3 animations and transitions can be used together and the animate service will figure out the correct duration and delay timing. - * - * Upon DOM mutation, the event class is added first (something like `ng-enter`), then the browser prepares itself to add - * the active class (in this case `ng-enter-active`) which then triggers the animation. The animation module will automatically - * detect the CSS code to determine when the animation ends. Once the animation is over then both CSS classes will be - * removed from the DOM. If a browser does not support CSS transitions or CSS animations then the animation will start and end - * immediately resulting in a DOM element that is at its final state. This final state is when the DOM element - * has no CSS transition/animation classes applied to it. - * - * ### Structural transition animations - * - * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition - * value to force the browser into rendering the styles defined in the setup (`.ng-enter`, `.ng-leave` - * or `.ng-move`) class. This means that any active transition animations operating on the element - * will be cut off to make way for the enter, leave or move animation. - * - * ### Class-based transition animations - * - * Class-based transitions refer to transition animations that are triggered when a CSS class is - * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`, - * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`). - * They are different when compared to structural animations since they **do not cancel existing - * animations** nor do they **block successive transitions** from rendering on the same element. - * This distinction allows for **multiple class-based transitions** to be performed on the same element. - * - * In addition to ngAnimate supporting the default (natural) functionality of class-based transition - * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the - * developer in further styling the element throughout the transition animation. Earlier versions - * of ngAnimate may have caused natural CSS transitions to break and not render properly due to - * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class - * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of - * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS - * class transitions are compatible with ngAnimate. - * - * There is, however, one special case when dealing with class-based transitions in ngAnimate. - * When rendering class-based transitions that make use of the setup and active CSS classes - * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define - * the transition value **on the active CSS class** and not the setup class. + * Since we're adding/removing CSS classes then the CSS transition will also pick those up: * * ```css - * .fade-add { - * /* remember to place a 0s transition here - * to ensure that the styles are applied instantly - * even if the element already has a transition style */ - * transition:0s linear all; - * - * /* starting CSS styles */ - * opacity:1; - * } - * .fade-add.fade-add-active { - * /* this will be the length of the animation */ - * transition:1s linear all; - * opacity:0; + * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code, + * the CSS classes below will be transitioned despite them being defined as regular CSS classes */ + * .red { background:red; } + * .large-text { font-size:20px; } + * + * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */ + * .pulse-twice { + * animation: 0.5s pulse linear 2; + * -webkit-animation: 0.5s pulse linear 2; * } - * ``` - * - * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it - * has a duration of zero. This may not be required, however, incase the browser is unable to render - * the styling present in this CSS class instantly then it could be that the browser is attempting - * to perform an unnecessary transition. * - * This workaround, however, does not apply to standard class-based transitions that are rendered - * when a CSS class containing a transition is applied to an element: + * @keyframes pulse { + * from { transform: scale(0.5); } + * to { transform: scale(1.5); } + * } * - * ```css - * /* this works as expected */ - * .fade { - * transition:1s linear all; - * opacity:0; + * @-webkit-keyframes pulse { + * from { -webkit-transform: scale(0.5); } + * to { -webkit-transform: scale(1.5); } * } * ``` * - * Please keep this in mind when coding the CSS markup that will be used within class-based transitions. - * Also, try not to mix the two class-based animation flavors together since the CSS code may become - * overly complex. - * - * - * ### Preventing Collisions With Third Party Libraries - * - * Some third-party frameworks place animation duration defaults across many element or className - * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which - * is expecting actual animations on these elements and has to wait for their completion. + * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen. * - * You can prevent this unwanted behavior by using a prefix on all your animation classes: - * - * ```css - * /* prefixed with animate- */ - * .animate-fade-add.animate-fade-add-active { - * transition:1s linear all; - * opacity:0; - * } - * ``` + * ## How the Options are handled * - * You then configure `$animate` to enforce this prefix: + * `$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 - * $animateProvider.classNameFilter(/animate-/); + * var animator = $animateCss(element, { + * from: { background:'red' }, + * to: { background:'blue' } + * }); + * animator.start(); * ``` - * </div> - * - * ### CSS Staggering Animations - * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a - * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be - * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for - * the animation. The style property expected within the stagger class can either be a **transition-delay** or an - * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). * * ```css - * .my-animation.ng-enter { - * /* standard transition code */ - * -webkit-transition: 1s linear all; - * transition: 1s linear all; - * opacity:0; + * .rotating-animation { + * animation:0.5s rotate linear; + * -webkit-animation:0.5s rotate linear; * } - * .my-animation.ng-enter-stagger { - * /* this will have a 100ms delay between each successive leave animation */ - * -webkit-transition-delay: 0.1s; - * transition-delay: 0.1s; * - * /* in case the stagger doesn't work then these two values - * must be set to 0 to avoid an accidental CSS inheritance */ - * -webkit-transition-duration: 0s; - * transition-duration: 0s; + * @keyframes rotate { + * from { transform: rotate(0deg); } + * to { transform: rotate(360deg); } * } - * .my-animation.ng-enter.ng-enter-active { - * /* standard transition styles */ - * opacity:1; + * + * @-webkit-keyframes rotate { + * from { -webkit-transform: rotate(0deg); } + * to { -webkit-transform: rotate(360deg); } * } * ``` * - * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations - * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this - * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation - * will also be reset if more than 10ms has passed after the last animation has been fired. - * - * The following code will issue the **ng-leave-stagger** event on the element provided: - * - * ```js - * var kids = parent.children(); - * - * $animate.leave(kids[0]); //stagger index=0 - * $animate.leave(kids[1]); //stagger index=1 - * $animate.leave(kids[2]); //stagger index=2 - * $animate.leave(kids[3]); //stagger index=3 - * $animate.leave(kids[4]); //stagger index=4 - * - * $timeout(function() { - * //stagger has reset itself - * $animate.leave(kids[5]); //stagger index=0 - * $animate.leave(kids[6]); //stagger index=1 - * }, 100, false); - * ``` + * 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. * - * Stagger animations are currently only supported within CSS-defined animations. + * ## What is returned * - * ## JavaScript-defined Animations - * In the event that you do not want to use CSS3 transitions or CSS3 animations or if you wish to offer animations on browsers that do not - * yet support CSS transitions/animations, then you can make use of JavaScript animations defined inside of your AngularJS module. + * `$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 - * //!annotate="YourApp" Your AngularJS Module|Replace this or ngModule with the module that you used to define your application. - * var ngModule = angular.module('YourApp', ['ngAnimate']); - * ngModule.animation('.my-crazy-animation', function() { - * return { - * enter: function(element, done) { - * //run the animation here and call done when the animation is complete - * return function(cancelled) { - * //this (optional) function will be called when the animation - * //completes or when the animation is cancelled (the cancelled - * //flag will be set to true if cancelled). - * }; - * }, - * leave: function(element, done) { }, - * move: function(element, done) { }, - * - * //animation that can be triggered before the class is added - * beforeAddClass: function(element, className, done) { }, - * - * //animation that can be triggered after the class is added - * addClass: function(element, className, done) { }, - * - * //animation that can be triggered before the class is removed - * beforeRemoveClass: function(element, className, done) { }, - * - * //animation that can be triggered after the class is removed - * removeClass: function(element, className, done) { } - * }; - * }); + * var animator = $animateCss(element, { ... }); * ``` * - * JavaScript-defined animations are created with a CSS-like class selector and a collection of events which are set to run - * a javascript callback function. When an animation is triggered, $animate will look for a matching animation which fits - * the element's CSS class attribute value and then run the matching animation event function (if found). - * In other words, if the CSS classes present on the animated element match any of the JavaScript animations then the callback function will - * be executed. It should be also noted that only simple, single class selectors are allowed (compound class selectors are not supported). - * - * Within a JavaScript animation, an object containing various event callback animation functions is expected to be returned. - * As explained above, these callbacks are triggered based on the animation event. Therefore if an enter animation is run, - * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation - * or transition code that is defined via a stylesheet). - * - * - * ### Applying Directive-specific Styles to an Animation - * In some cases a directive or service may want to provide `$animate` with extra details that the animation will - * include into its animation. Let's say for example we wanted to render an animation that animates an element - * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click - * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function - * call to `$animate.addClass`. + * Now what do the contents of our `animator` variable look like: * * ```js - * canvas.on('click', function(e) { - * $animate.addClass(element, 'on', { - * to: { - * left : e.client.x + 'px', - * top : e.client.y + 'px' - * } - * }): - * }); - * ``` + * { + * // starts the animation + * start: Function, * - * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will - * also include and transition the styling of the `left` and `top` properties into its running animation. If we want - * to provide some starting animation values then we can do so by placing the starting animations styles into an object - * called `from` in the same object as the `to` animations. - * - * ```js - * canvas.on('click', function(e) { - * $animate.addClass(element, 'on', { - * from: { - * position: 'absolute', - * left: '0px', - * top: '0px' - * }, - * to: { - * left : e.client.x + 'px', - * top : e.client.y + 'px' - * } - * }): - * }); + * // ends (aborts) the animation + * end: Function + * } * ``` * - * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the - * element. If `ngAnimate` is not present then the styles will be applied immediately. - * + * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. + * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and stlyes may have been + * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties + * and that changing them will not reconfigure the parameters of the animation. + * + * ### 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 occuring 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 colapse 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 BASE_TEN = 10; + +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); + } -angular.module('ngAnimate', ['ng']) - - /** - * @ngdoc provider - * @name $animateProvider - * @description - * - * The `$animateProvider` allows developers to register JavaScript animation event handlers directly inside of a module. - * When an animation is triggered, the $animate service will query the $animate service to find any animations that match - * the provided name value. - * - * Requires the {@link ngAnimate `ngAnimate`} module to be installed. - * - * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. - * - */ - .directive('ngAnimateChildren', function() { - var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; - return function(scope, element, attrs) { - var val = attrs.ngAnimateChildren; - if (angular.isString(val) && val.length === 0) { //empty attribute - element.data(NG_ANIMATE_CHILDREN, true); + // by setting this to null in the event that the delay is not set or is set directly as 0 + // then we can still allow for zegative values to be used later on and not mistake this + // value for being greater than any other negative value. + if (val === 0) { + val = null; + } + styles[actualStyleName] = val; + } + }); + + return styles; +} + +function parseMaxTime(str) { + var maxValue = 0; + var values = str.split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; +} + +function truthyTimingValue(val) { + return val === 0 || val != null; +} + +function getCssTransitionDurationStyle(duration, applyOnlyDuration) { + var style = TRANSITION_PROP; + var value = duration + 's'; + if (applyOnlyDuration) { + style += DURATION_KEY; + } else { + value += ' linear all'; + } + return [style, value]; +} + +function 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 { - scope.$watch(val, function(value) { - element.data(NG_ANIMATE_CHILDREN, !!value); - }); + cache[key].total++; } - }; - }) - - //this private service is only used within CSS-enabled animations - //IE8 + IE9 do not support rAF natively, but that is fine since they - //also don't support transitions and keyframes which means that the code - //below will never be used by the two browsers. - .factory('$$animateReflow', ['$$rAF', '$document', function($$rAF, $document) { - var bod = $document[0].body; - return function(fn) { - //the returned function acts as the cancellation function - return $$rAF(function() { - //the line below will force the browser to perform a repaint - //so that all the animated elements within the animation frame - //will be properly updated and drawn on screen. This is - //required to perform multi-class CSS based animations with - //Firefox. DO NOT REMOVE THIS LINE. DO NOT OPTIMIZE THIS LINE. - //THE MINIFIER WILL REMOVE IT OTHERWISE WHICH WILL RESULT IN AN - //UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND WILL - //TAKE YEARS AWAY FROM YOUR LIFE! - fn(bod.offsetWidth); - }); - }; - }]) - - .config(['$provide', '$animateProvider', function($provide, $animateProvider) { - var noop = angular.noop; - var forEach = angular.forEach; - var selectors = $animateProvider.$$selectors; - var isArray = angular.isArray; - var isString = angular.isString; - var isObject = angular.isObject; + } + }; +} + +// 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', 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; + } - var ELEMENT_NODE = 1; - var NG_ANIMATE_STATE = '$$ngAnimateState'; - var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; - var NG_ANIMATE_CLASS_NAME = 'ng-animate'; - var rootAnimateState = {running: true}; + function computeCachedCssStyles(node, className, cacheKey, properties) { + var timings = gcsLookup.get(cacheKey); - function extractElementNode(element) { - for (var i = 0; i < element.length; i++) { - var elm = element[i]; - if (elm.nodeType == ELEMENT_NODE) { - return elm; + if (!timings) { + timings = computeCssStyles($window, node, properties); + if (timings.animationIterationCount === 'infinite') { + timings.animationIterationCount = 1; } } + + // we keep putting this in multiple times even though the value and the cacheKey are the same + // because we're keeping an interal tally of how many duplicate animations are detected. + gcsLookup.put(cacheKey, timings); + return timings; } - function prepareElement(element) { - return element && angular.element(element); + 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 || {}; } - function stripCommentsFromElement(element) { - return angular.element(extractElementNode(element)); + var cancelLastRAFRequest; + 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 isMatchingElement(elm1, elm2) { - return extractElementNode(elm1) == extractElementNode(elm2); + 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; } - var $$jqLite; - $provide.decorator('$animate', - ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', '$$jqLite', - function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest, $$$jqLite) { - - $$jqLite = $$$jqLite; - $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); - - // Wait until all directive and route-related templates are downloaded and - // compiled. The $templateRequest.totalPendingRequests variable keeps track of - // all of the remote templates being currently downloaded. If there are no - // templates currently downloading then the watcher will still fire anyway. - var deregisterWatch = $rootScope.$watch( - function() { return $templateRequest.totalPendingRequests; }, - function(val, oldVal) { - if (val !== 0) return; - deregisterWatch(); - - // Now that all templates have been downloaded, $animate will wait until - // the post digest queue is empty before enabling animations. By having two - // calls to $postDigest calls we can ensure that the flag is enabled at the - // very end of the post digest queue. Since all of the animations in $animate - // use $postDigest, it's important that the code below executes at the end. - // This basically means that the page is fully downloaded and compiled before - // any animations are triggered. - $rootScope.$$postDigest(function() { - $rootScope.$$postDigest(function() { - rootAnimateState.running = false; - }); - }); - } - ); - var globalAnimationCounter = 0; - var classNameFilter = $animateProvider.classNameFilter(); - var isAnimatableClassName = !classNameFilter - ? function() { return true; } - : function(className) { - return classNameFilter.test(className); - }; + 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; + } - function classBasedAnimationsBlocked(element, setter) { - var data = element.data(NG_ANIMATE_STATE) || {}; - if (setter) { - data.running = true; - data.structural = true; - element.data(NG_ANIMATE_STATE, data); + if (options.addClass) { + addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); + } + + if (options.removeClass) { + if (addRemoveClassName.length) { + addRemoveClassName += ' '; } - return data.disabled || (data.running && data.structural); + addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); } - function runAnimationPostDigest(fn) { - var cancelFn, defer = $$q.defer(); - defer.promise.$$cancelFn = function() { - cancelFn && cancelFn(); + // 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 }; - $rootScope.$$postDigest(function() { - cancelFn = fn(function() { - defer.resolve(); - }); - }); - return defer.promise; + } else { + cacheKey = gcsHashFn(node, fullClassName); + stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); } - function parseAnimateOptions(options) { - // some plugin code may still be passing in the callback - // function as the last param for the $animate methods so - // it's best to only allow string or array values for now - if (isObject(options)) { - if (options.tempClasses && isString(options.tempClasses)) { - options.tempClasses = options.tempClasses.split(/\s+/); - } - return options; + 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)); } } - function resolveElementClasses(element, cache, runningAnimations) { - runningAnimations = runningAnimations || {}; + if (maxDuration === 0 && !flags.recalculateTimingStyles) { + return closeAndReturnNoopAnimator(); + } - var lookup = {}; - forEach(runningAnimations, function(data, selector) { - forEach(selector.split(' '), function(s) { - lookup[s]=data; - }); - }); + 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); + } - var hasClasses = Object.create(null); - forEach((element.attr('class') || '').split(/\s+/), function(className) { - hasClasses[className] = true; - }); + if (flags.applyTransitionDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle)); + } - var toAdd = [], toRemove = []; - forEach((cache && cache.classes) || [], function(status, className) { - var hasClass = hasClasses[className]; - var matchingAnimation = lookup[className] || {}; - - // When addClass and removeClass is called then $animate will check to - // see if addClass and removeClass cancel each other out. When there are - // more calls to removeClass than addClass then the count falls below 0 - // and then the removeClass animation will be allowed. Otherwise if the - // count is above 0 then that means an addClass animation will commence. - // Once an animation is allowed then the code will also check to see if - // there exists any on-going animation that is already adding or remvoing - // the matching CSS class. - if (status === false) { - //does it have the class or will it have the class - if (hasClass || matchingAnimation.event == 'addClass') { - toRemove.push(className); - } - } else if (status === true) { - //is the class missing or will it be removed? - if (!hasClass || matchingAnimation.event == 'removeClass') { - toAdd.push(className); - } - } - }); + if (flags.applyAnimationDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle, true)); + } + } - return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')]; - } - - function lookup(name) { - if (name) { - var matches = [], - flagMap = {}, - classes = name.substr(1).split('.'); - - //the empty string value is the default animation - //operation which performs CSS transition and keyframe - //animations sniffing. This is always included for each - //element animation procedure if the browser supports - //transitions and/or keyframe animations. The default - //animation is added to the top of the list to prevent - //any previous animations from affecting the element styling - //prior to the element being animated. - if ($sniffer.transitions || $sniffer.animations) { - matches.push($injector.get(selectors[''])); - } + // 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; + } - for (var i=0; i < classes.length; i++) { - var klass = classes[i], - selectorFactoryName = selectors[klass]; - if (selectorFactoryName && !flagMap[klass]) { - matches.push($injector.get(selectorFactoryName)); - flagMap[klass] = true; - } - } - return matches; + 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); } - function animationRunner(element, animationEvent, className, options) { - //transcluded directives may sometimes fire an animation using only comment nodes - //best to catch this early on to prevent any animation operations from occurring - var node = element[0]; - if (!node) { - return; + if (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(); + } - if (options) { - options.to = options.to || {}; - options.from = options.from || {}; + function cancelFn() { + close(true); + } + + function close(rejected) { // jshint ignore:line + // if the promise has been called already then we shouldn't close + // the animation again + if (animationClosed || (animationCompleted && animationPaused)) return; + animationClosed = true; + animationPaused = false; + + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); } + $$jqLite.removeClass(element, activeClasses); - var classNameAdd; - var classNameRemove; - if (isArray(className)) { - classNameAdd = className[0]; - classNameRemove = className[1]; - if (!classNameAdd) { - className = classNameRemove; - animationEvent = 'removeClass'; - } else if (!classNameRemove) { - className = classNameAdd; - animationEvent = 'addClass'; - } else { - className = classNameAdd + ' ' + classNameRemove; - } + 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) { + value ? node.style.setProperty(prop, value) + : node.style.removeProperty(prop); + }); } - var isSetClassOperation = animationEvent == 'setClass'; - var isClassBased = isSetClassOperation - || animationEvent == 'addClass' - || animationEvent == 'removeClass' - || animationEvent == 'animate'; + // 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(); + } - var currentClassName = element.attr('class'); - var classes = currentClassName + ' ' + className; - if (!isAnimatableClassName(classes)) { - return; + if (events && events.length) { + // Remove the transitionend / animationend listener(s) + element.off(events.join(' '), onAnimationProgress); } - var beforeComplete = noop, - beforeCancel = [], - before = [], - afterComplete = noop, - afterCancel = [], - after = []; + //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); + } - var animationLookup = (' ' + classes).replace(/\s+/g,'.'); - forEach(lookup(animationLookup), function(animationFactory) { - var created = registerAnimation(animationFactory, animationEvent); - if (!created && isSetClassOperation) { - registerAnimation(animationFactory, 'addClass'); - registerAnimation(animationFactory, 'removeClass'); - } + // 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 }); - function registerAnimation(animationFactory, event) { - var afterFn = animationFactory[event]; - var beforeFn = animationFactory['before' + event.charAt(0).toUpperCase() + event.substr(1)]; - if (afterFn || beforeFn) { - if (event == 'leave') { - beforeFn = afterFn; - //when set as null then animation knows to skip this phase - afterFn = null; + // 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; + + // 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); + animationPaused + ? temporaryStyles.push(value) + : removeFromArray(temporaryStyles, value); } - after.push({ - event: event, fn: afterFn - }); - before.push({ - event: event, fn: beforeFn - }); - return true; + } else if (animationPaused && playAnimation) { + animationPaused = false; + close(); } + }; + + // checking the stagger duration prevents an accidently cascade of the CSS delay style + // being inherited from the parent. If the transition duration is zero then we can safely + // rely that the delay value is an intential stagger delay style. + var maxStagger = itemIndex > 0 + && ((timings.transitionDuration && stagger.transitionDuration === 0) || + (timings.animationDuration && stagger.animationDuration === 0)) + && Math.max(stagger.animationDelay, stagger.transitionDelay); + if (maxStagger) { + $timeout(triggerAnimationStart, + Math.floor(maxStagger * itemIndex * ONE_SECOND), + false); + } else { + triggerAnimationStart(); } - function run(fns, cancellations, allCompleteFn) { - var animations = []; - forEach(fns, function(animation) { - animation.fn && animations.push(animation); + // this will decorate the existing promise runner with pause/resume methods + runnerHost.resume = function() { + playPause(true); + }; + + runnerHost.pause = function() { + playPause(false); + }; + + function triggerAnimationStart() { + // just incase a stagger animation kicks in when the animation + // itself was cancelled entirely + if (animationClosed) return; + + applyBlocking(false); + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[key] = value; }); - var count = 0; - function afterAnimationComplete(index) { - if (cancellations) { - (cancellations[index] || noop)(); - if (++count < animations.length) return; - cancellations = null; + applyAnimationClasses(element, options); + $$jqLite.addClass(element, activeClasses); + + if (flags.recalculateTimingStyles) { + fullClassName = node.className + ' ' + preparationClasses; + cacheKey = gcsHashFn(node, fullClassName); + + timings = computeTimings(node, fullClassName, cacheKey); + relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + if (maxDuration === 0) { + close(); + return; } - allCompleteFn(); + + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; } - //The code below adds directly to the array in order to work with - //both sync and async animations. Sync animations are when the done() - //operation is called right away. DO NOT REFACTOR! - forEach(animations, function(animation, index) { - var progress = function() { - afterAnimationComplete(index); - }; - switch (animation.event) { - case 'setClass': - cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress, options)); - break; - case 'animate': - cancellations.push(animation.fn(element, className, options.from, options.to, progress)); - break; - case 'addClass': - cancellations.push(animation.fn(element, classNameAdd || className, progress, options)); - break; - case 'removeClass': - cancellations.push(animation.fn(element, classNameRemove || className, progress, options)); - break; - default: - cancellations.push(animation.fn(element, progress, options)); - break; + if (flags.applyAnimationDelay) { + relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + ? parseFloat(options.delay) + : relativeDelay; + + maxDelay = Math.max(relativeDelay, 0); + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + + if (options.easing) { + var easeProp, easeVal = options.easing; + if (flags.hasTransitions) { + easeProp = TRANSITION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; } - }); + if (flags.hasAnimations) { + easeProp = ANIMATION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + } - if (cancellations && cancellations.length === 0) { - allCompleteFn(); + if (timings.transitionDuration) { + events.push(TRANSITIONEND_EVENT); } - } - return { - node: node, - event: animationEvent, - className: className, - isClassBased: isClassBased, - isSetClassOperation: isSetClassOperation, - applyStyles: function() { - if (options) { - element.css(angular.extend(options.from || {}, options.to || {})); + if (timings.animationDuration) { + events.push(ANIMATIONEND_EVENT); + } + + startTime = Date.now(); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); } - }, - before: function(allCompleteFn) { - beforeComplete = allCompleteFn; - run(before, beforeCancel, function() { - beforeComplete = noop; - allCompleteFn(); - }); - }, - after: function(allCompleteFn) { - afterComplete = allCompleteFn; - run(after, afterCancel, function() { - afterComplete = noop; - allCompleteFn(); - }); - }, - cancel: function() { - if (beforeCancel) { - forEach(beforeCancel, function(cancelFn) { - (cancelFn || noop)(true); - }); - beforeComplete(true); + } + + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } + + if (events.length) { + element.on(events.join(' '), onAnimationProgress); + } + + if (options.to) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); } - if (afterCancel) { - forEach(afterCancel, function(cancelFn) { - (cancelFn || noop)(true); - }); - afterComplete(true); + applyAnimationToStyles(element, options); + } + } + + function onAnimationExpired() { + var animationsData = element.data(ANIMATE_TIMER_KEY); + + // this will be false in the event that the element was + // removed from the DOM (via a leave animation or something + // similar) + if (animationsData) { + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); } + element.removeData(ANIMATE_TIMER_KEY); } - }; + } + } + }; + }]; +}]; + +var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateCssDriver'); + + var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; + var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; + + var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; + var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; + + function isDocumentFragment(node) { + return node.parentNode && node.parentNode.nodeType === 11; + } + + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { + + // only browsers that support these properties can render animations + if (!$sniffer.animations && !$sniffer.transitions) return noop; + + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var rootBodyElement = jqLite( + // this is to avoid using something that exists outside of the body + // we also special case the doc fragement case because our unit test code + // appends the $rootElement to the body after the app has been bootstrapped + isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode + ); + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + return function initDriverFn(animationDetails) { + return animationDetails.from && animationDetails.to + ? prepareFromToAnchorAnimation(animationDetails.from, + animationDetails.to, + animationDetails.classes, + animationDetails.anchors) + : prepareRegularAnimation(animationDetails); + }; + + function filterCssClasses(classes) { + //remove all the `ng-` stuff + return classes.replace(/\bng-\S+\b/g, ''); + } + + function getUniqueValues(a, b) { + if (isString(a)) a = a.split(' '); + if (isString(b)) b = b.split(' '); + return a.filter(function(val) { + return b.indexOf(val) === -1; + }).join(' '); + } + + function prepareAnchoredAnimation(classes, outAnchor, inAnchor) { + var clone = jqLite(getDomNode(outAnchor).cloneNode(true)); + var startingClasses = filterCssClasses(getClassVal(clone)); + + outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + + clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME); + + rootBodyElement.append(clone); + + var animatorIn, animatorOut = prepareOutAnimation(); + + // the user may not end up using the `out` animation and + // only making use of the `in` animation or vice-versa. + // In either case we should allow this and not assume the + // animation is over unless both animations are not used. + if (!animatorOut) { + animatorIn = prepareInAnimation(); + if (!animatorIn) { + return end(); + } } - /** - * @ngdoc service - * @name $animate - * @kind object - * - * @description - * The `$animate` service provides animation detection support while performing DOM operations (enter, leave and move) as well as during addClass and removeClass operations. - * When any of these operations are run, the $animate service - * will examine any JavaScript-defined animations (which are defined by using the $animateProvider provider object) - * as well as any CSS-defined animations against the CSS classes present on the element once the DOM operation is run. - * - * The `$animate` service is used behind the scenes with pre-existing directives and animation with these directives - * will work out of the box without any extra configuration. - * - * Requires the {@link ngAnimate `ngAnimate`} module to be installed. - * - * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. - * ## Callback Promises - * With AngularJS 1.3, each of the animation methods, on the `$animate` service, return a promise when called. The - * promise itself is then resolved once the animation has completed itself, has been cancelled or has been - * skipped due to animations being disabled. (Note that even if the animation is cancelled it will still - * call the resolve function of the animation.) - * - * ```js - * $animate.enter(element, container).then(function() { - * //...this is called once the animation is complete... - * }); - * ``` - * - * Also note that, due to the nature of the callback promise, if any Angular-specific code (like changing the scope, - * location of the page, etc...) is executed within the callback promise then be sure to wrap the code using - * `$scope.$apply(...)`; - * - * ```js - * $animate.leave(element).then(function() { - * $scope.$apply(function() { - * $location.path('/new-page'); - * }); - * }); - * ``` - * - * An animation can also be cancelled by calling the `$animate.cancel(promise)` method with the provided - * promise that was returned when the animation was started. - * - * ```js - * var promise = $animate.addClass(element, 'super-long-animation'); - * promise.then(function() { - * //this will still be called even if cancelled - * }); - * - * element.on('click', function() { - * //tooo lazy to wait for the animation to end - * $animate.cancel(promise); - * }); - * ``` - * - * (Keep in mind that the promise cancellation is unique to `$animate` since promises in - * general cannot be cancelled.) - * - */ + var startingAnimator = animatorOut || animatorIn; + return { - /** - * @ngdoc method - * @name $animate#animate - * @kind function - * - * @description - * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element. - * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation - * will take on the provided styles. For example, if a transition animation is set for the given className then the - * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is - * detected then the provided styles will be given in as function paramters. - * - * ```js - * ngModule.animation('.my-inline-animation', function() { - * return { - * animate : function(element, className, from, to, done) { - * //styles - * } - * } - * }); - * ``` - * - * Below is a breakdown of each step that occurs during the `animate` animation: - * - * | Animation Step | What the element class attribute looks like | - * |-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| - * | 1. `$animate.animate(...)` is called | `class="my-animation"` | - * | 2. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` | - * | 3. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | - * | 4. the `className` class value is added to the element | `class="my-animation ng-animate className"` | - * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate className"` | - * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.className` class styling is applied right away| `class="my-animation ng-animate className"` | - * | 7. `$animate` applies the provided collection of `from` CSS styles to the element | `class="my-animation ng-animate className"` | - * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate className"` | - * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate className"` | - * | 10. the `className-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate className className-active"` | - * | 11. `$animate` applies the collection of `to` CSS styles to the element which are then handled by the transition | `class="my-animation ng-animate className className-active"` | - * | 12. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate className className-active"` | - * | 13. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | - * | 14. The returned promise is resolved. | `class="my-animation"` | - * - * @param {DOMElement} element the element that will be the focus of the enter animation - * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation - * @param {object} to a collection of CSS styles that the element will animate towards - * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`) - * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - animate: function(element, from, to, className, options) { - className = className || 'ng-inline-animate'; - options = parseAnimateOptions(options) || {}; - options.from = to ? from : null; - options.to = to ? to : from; - - return runAnimationPostDigest(function(done) { - return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done); + start: function() { + var runner; + + var currentAnimation = startingAnimator.start(); + currentAnimation.done(function() { + currentAnimation = null; + if (!animatorIn) { + animatorIn = prepareInAnimation(); + if (animatorIn) { + currentAnimation = animatorIn.start(); + currentAnimation.done(function() { + currentAnimation = null; + end(); + runner.complete(); + }); + return currentAnimation; + } + } + // in the event that there is no `in` animation + end(); + runner.complete(); }); - }, - /** - * @ngdoc method - * @name $animate#enter - * @kind function - * - * @description - * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once - * the animation is started, the following CSS classes will be present on the element for the duration of the animation: - * - * Below is a breakdown of each step that occurs during enter animation: - * - * | Animation Step | What the element class attribute looks like | - * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| - * | 1. `$animate.enter(...)` is called | `class="my-animation"` | - * | 2. element is inserted into the `parentElement` element or beside the `afterElement` element | `class="my-animation"` | - * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` | - * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | - * | 5. the `.ng-enter` class is added to the element | `class="my-animation ng-animate ng-enter"` | - * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-enter"` | - * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-enter` class styling is applied right away | `class="my-animation ng-animate ng-enter"` | - * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-enter"` | - * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-enter"` | - * | 10. the `.ng-enter-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-enter ng-enter-active"` | - * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-enter ng-enter-active"` | - * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | - * | 13. The returned promise is resolved. | `class="my-animation"` | - * - * @param {DOMElement} element the element that will be the focus of the enter animation - * @param {DOMElement} parentElement the parent element of the element that will be the focus of the enter animation - * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the enter animation - * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - enter: function(element, parentElement, afterElement, options) { - options = parseAnimateOptions(options); - element = angular.element(element); - parentElement = prepareElement(parentElement); - afterElement = prepareElement(afterElement); - - classBasedAnimationsBlocked(element, true); - $delegate.enter(element, parentElement, afterElement); - return runAnimationPostDigest(function(done) { - return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); + runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn }); - }, - /** - * @ngdoc method - * @name $animate#leave - * @kind function - * - * @description - * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once - * the animation is started, the following CSS classes will be added for the duration of the animation: - * - * Below is a breakdown of each step that occurs during leave animation: - * - * | Animation Step | What the element class attribute looks like | - * |-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------| - * | 1. `$animate.leave(...)` is called | `class="my-animation"` | - * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | - * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` | - * | 4. the `.ng-leave` class is added to the element | `class="my-animation ng-animate ng-leave"` | - * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-leave"` | - * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.ng-leave` class styling is applied right away | `class="my-animation ng-animate ng-leave"` | - * | 7. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-leave"` | - * | 8. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-leave"` | - * | 9. the `.ng-leave-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-leave ng-leave-active"` | - * | 10. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-leave ng-leave-active"` | - * | 11. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | - * | 12. The element is removed from the DOM | ... | - * | 13. The returned promise is resolved. | ... | - * - * @param {DOMElement} element the element that will be the focus of the leave animation - * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - leave: function(element, options) { - options = parseAnimateOptions(options); - element = angular.element(element); - - cancelChildAnimations(element); - classBasedAnimationsBlocked(element, true); - return runAnimationPostDigest(function(done) { - return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { - $delegate.leave(element); - }, options, done); - }); - }, + return runner; - /** - * @ngdoc method - * @name $animate#move - * @kind function - * - * @description - * Fires the move DOM operation. Just before the animation starts, the animate service will either append it into the parentElement container or - * add the element directly after the afterElement element if present. Then the move animation will be run. Once - * the animation is started, the following CSS classes will be added for the duration of the animation: - * - * Below is a breakdown of each step that occurs during move animation: - * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------| - * | 1. `$animate.move(...)` is called | `class="my-animation"` | - * | 2. element is moved into the parentElement element or beside the afterElement element | `class="my-animation"` | - * | 3. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` | - * | 4. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | - * | 5. the `.ng-move` class is added to the element | `class="my-animation ng-animate ng-move"` | - * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate ng-move"` | - * | 7. `$animate` blocks all CSS transitions on the element to ensure the `.ng-move` class styling is applied right away | `class="my-animation ng-animate ng-move"` | - * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate ng-move"` | - * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate ng-move"` | - * | 10. the `.ng-move-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate ng-move ng-move-active"` | - * | 11. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate ng-move ng-move-active"` | - * | 12. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | - * | 13. The returned promise is resolved. | `class="my-animation"` | - * - * @param {DOMElement} element the element that will be the focus of the move animation - * @param {DOMElement} parentElement the parentElement element of the element that will be the focus of the move animation - * @param {DOMElement} afterElement the sibling element (which is the previous element) of the element that will be the focus of the move animation - * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - move: function(element, parentElement, afterElement, options) { - options = parseAnimateOptions(options); - element = angular.element(element); - parentElement = prepareElement(parentElement); - afterElement = prepareElement(afterElement); - - cancelChildAnimations(element); - classBasedAnimationsBlocked(element, true); - $delegate.move(element, parentElement, afterElement); - return runAnimationPostDigest(function(done) { - return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); - }); - }, + function endFn() { + if (currentAnimation) { + currentAnimation.end(); + } + } + } + }; - /** - * @ngdoc method - * @name $animate#addClass - * - * @description - * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. - * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide - * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions - * or keyframes are defined on the -add-active or base CSS class). - * - * Below is a breakdown of each step that occurs during addClass animation: - * - * | Animation Step | What the element class attribute looks like | - * |--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| - * | 1. `$animate.addClass(element, 'super')` is called | `class="my-animation"` | - * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | - * | 3. the `.super-add` class is added to the element | `class="my-animation ng-animate super-add"` | - * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate super-add"` | - * | 5. the `.super` and `.super-add-active` classes are added (this triggers the CSS transition/animation) | `class="my-animation ng-animate super super-add super-add-active"` | - * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super super-add super-add-active"` | - * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super super-add super-add-active"` | - * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation super"` | - * | 9. The super class is kept on the element | `class="my-animation super"` | - * | 10. The returned promise is resolved. | `class="my-animation super"` | - * - * @param {DOMElement} element the element that will be animated - * @param {string} className the CSS class that will be added to the element and then animated - * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - addClass: function(element, className, options) { - return this.setClass(element, className, [], options); - }, + function calculateAnchorStyles(anchor) { + var styles = {}; + + var coords = getDomNode(anchor).getBoundingClientRect(); + + // we iterate directly since safari messes up and doesn't return + // all the keys for the coods object when iterated + forEach(['width','height','top','left'], function(key) { + var value = coords[key]; + switch (key) { + case 'top': + value += bodyNode.scrollTop; + break; + case 'left': + value += bodyNode.scrollLeft; + break; + } + styles[key] = Math.floor(value) + 'px'; + }); + return styles; + } - /** - * @ngdoc method - * @name $animate#removeClass - * - * @description - * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value - * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in - * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if - * no CSS transitions or keyframes are defined on the -remove or base CSS classes). - * - * Below is a breakdown of each step that occurs during removeClass animation: - * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------| - * | 1. `$animate.removeClass(element, 'super')` is called | `class="my-animation super"` | - * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation super ng-animate"` | - * | 3. the `.super-remove` class is added to the element | `class="my-animation super ng-animate super-remove"` | - * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation super ng-animate super-remove"` | - * | 5. the `.super-remove-active` classes are added and `.super` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate super-remove super-remove-active"` | - * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate super-remove super-remove-active"` | - * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate super-remove super-remove-active"` | - * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | - * | 9. The returned promise is resolved. | `class="my-animation"` | - * - * - * @param {DOMElement} element the element that will be animated - * @param {string} className the CSS class that will be animated and then removed from the element - * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - removeClass: function(element, className, options) { - return this.setClass(element, [], className, options); - }, + function prepareOutAnimation() { + var animator = $animateCss(clone, { + addClass: NG_OUT_ANCHOR_CLASS_NAME, + delay: true, + from: calculateAnchorStyles(outAnchor) + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function getClassVal(element) { + return element.attr('class') || ''; + } + + function prepareInAnimation() { + var endingClasses = filterCssClasses(getClassVal(inAnchor)); + var toAdd = getUniqueValues(endingClasses, startingClasses); + var toRemove = getUniqueValues(startingClasses, endingClasses); + + var animator = $animateCss(clone, { + to: calculateAnchorStyles(inAnchor), + addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd, + removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove, + delay: true + }); - /** - * - * @ngdoc method - * @name $animate#setClass - * - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the `done()` callback will be fired (if provided). - * - * | Animation Step | What the element class attribute looks like | - * |----------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| - * | 1. `$animate.setClass(element, 'on', 'off')` is called | `class="my-animation off"` | - * | 2. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate off"` | - * | 3. the `.on-add` and `.off-remove` classes are added to the element | `class="my-animation ng-animate on-add off-remove off"` | - * | 4. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate on-add off-remove off"` | - * | 5. the `.on`, `.on-add-active` and `.off-remove-active` classes are added and `.off` is removed (this triggers the CSS transition/animation) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` | - * | 6. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` | - * | 7. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate on on-add on-add-active off-remove off-remove-active"` | - * | 8. The animation ends and all generated CSS classes are removed from the element | `class="my-animation on"` | - * | 9. The returned promise is resolved. | `class="my-animation on"` | - * - * @param {DOMElement} element the element which will have its CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * CSS classes have been set on the element - * @param {object=} options an optional collection of styles that will be picked up by the CSS transition/animation - * @return {Promise} the animation callback promise - */ - setClass: function(element, add, remove, options) { - options = parseAnimateOptions(options); - - var STORAGE_KEY = '$$animateClasses'; - element = angular.element(element); - element = stripCommentsFromElement(element); - - if (classBasedAnimationsBlocked(element)) { - return $delegate.$$setClassImmediately(element, add, remove, options); + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function end() { + clone.remove(); + outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + } + } + + function prepareFromToAnchorAnimation(from, to, classes, anchors) { + var fromAnimation = prepareRegularAnimation(from, noop); + var toAnimation = prepareRegularAnimation(to, noop); + + var anchorAnimations = []; + forEach(anchors, function(anchor) { + var outElement = anchor['out']; + var inElement = anchor['in']; + var animator = prepareAnchoredAnimation(classes, outElement, inElement); + if (animator) { + anchorAnimations.push(animator); + } + }); + + // no point in doing anything when there are no elements to animate + if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); } - // we're using a combined array for both the add and remove - // operations since the ORDER OF addClass and removeClass matters - var classes, cache = element.data(STORAGE_KEY); - var hasCache = !!cache; - if (!cache) { - cache = {}; - cache.classes = {}; + if (toAnimation) { + animationRunners.push(toAnimation.start()); } - classes = cache.classes; - add = isArray(add) ? add : add.split(' '); - forEach(add, function(c) { - if (c && c.length) { - classes[c] = true; - } + forEach(anchorAnimations, function(animation) { + animationRunners.push(animation.start()); }); - remove = isArray(remove) ? remove : remove.split(' '); - forEach(remove, function(c) { - if (c && c.length) { - classes[c] = false; - } + var runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn // CSS-driven animations cannot be cancelled, only ended }); - if (hasCache) { - if (options && cache.options) { - cache.options = angular.extend(cache.options || {}, options); - } + $$AnimateRunner.all(animationRunners, function(status) { + runner.complete(status); + }); + + return runner; + + function endFn() { + forEach(animationRunners, function(runner) { + runner.end(); + }); + } + } + }; + } + + function prepareRegularAnimation(animationDetails) { + var element = animationDetails.element; + var options = animationDetails.options || {}; + + if (animationDetails.structural) { + options.event = animationDetails.event; + options.structural = true; + options.applyClassesEarly = true; + + // we special case the leave animation since we want to ensure that + // the element is removed as soon as the animation is over. Otherwise + // a flicker might appear or the element may not be removed at all + if (animationDetails.event === 'leave') { + options.onDone = options.domOperation; + } + } + + // We assign the preparationClasses as the actual animation event since + // the internals of $animateCss will just suffix the event token values + // with `-active` to trigger the animation. + if (options.preparationClasses) { + options.event = concatWithSpace(options.event, options.preparationClasses); + } + + var animator = $animateCss(element, options); + + // the driver lookup code inside of $$animation attempts to spawn a + // driver one by one until a driver returns a.$$willAnimate animator object. + // $animateCss will always return an object, however, it will pass in + // a flag as a hint as to whether an animation was detected or not + return animator.$$willAnimate ? animator : null; + } + }]; +}]; + +// TODO(matsko): use caching here to speed things up for detection +// TODO(matsko): add documentation +// by the time... + +var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { + this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', + function($injector, $$AnimateRunner, $$jqLite) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + // $animateJs(element, 'enter'); + return function(element, event, classes, options) { + var animationClosed = false; + + // the `classes` argument is optional and if it is not used + // then the classes will be resolved from the element's className + // property as well as options.addClass/options.removeClass. + if (arguments.length === 3 && isObject(classes)) { + options = classes; + classes = null; + } + + options = prepareAnimationOptions(options); + if (!classes) { + classes = element.attr('class') || ''; + if (options.addClass) { + classes += ' ' + options.addClass; + } + if (options.removeClass) { + classes += ' ' + options.removeClass; + } + } + + var classesToAdd = options.addClass; + var classesToRemove = options.removeClass; + + // the lookupAnimations function returns a series of animation objects that are + // matched up with one or more of the CSS classes. These animation objects are + // defined via the module.animation factory function. If nothing is detected then + // we don't return anything which then makes $animation query the next driver. + var animations = lookupAnimations(classes); + var before, after; + if (animations.length) { + var afterFn, beforeFn; + if (event == 'leave') { + beforeFn = 'leave'; + afterFn = 'afterLeave'; // TODO(matsko): get rid of this + } else { + beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1); + afterFn = event; + } + + if (event !== 'enter' && event !== 'move') { + before = packageAnimations(element, event, options, animations, beforeFn); + } + after = packageAnimations(element, event, options, animations, afterFn); + } + + // no matching animations + if (!before && !after) return; + + function applyOptions() { + options.domOperation(); + applyAnimationClasses(element, options); + } + + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } - //the digest cycle will combine all the animations into one function - return cache.promise; + var runner; + + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); } else { - element.data(STORAGE_KEY, cache = { - classes: classes, - options: options + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, + start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); + var closeActiveAnimations; + var chain = []; + + if (before) { + chain.push(function(fn) { + closeActiveAnimations = before(fn); }); } - return cache.promise = runAnimationPostDigest(function(done) { - var cache, parentNode, parentElement, elementNode = extractElementNode(element); - if (elementNode) { - cache = element.data(STORAGE_KEY); - element.removeData(STORAGE_KEY); + if (chain.length) { + chain.push(function(fn) { + applyOptions(); + fn(true); + }); + } else { + applyOptions(); + } - parentElement = element.parent(); - parentNode = elementNode.parentNode; + if (after) { + chain.push(function(fn) { + closeActiveAnimations = after(fn); + }); + } + + runner.setHost({ + end: function() { + endAnimations(); + }, + cancel: function() { + endAnimations(true); } + }); - // TODO(matsko): move this code into the animationsDisabled() function once #8092 is fixed - if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) { - done(); - return; + $$AnimateRunner.chain(chain, onComplete); + return runner; + + function onComplete(success) { + close(success); + runner.complete(success); + } + + function endAnimations(cancelled) { + if (!animationClosed) { + (closeActiveAnimations || noop)(cancelled); + onComplete(cancelled); } + } + } + }; - var state = element.data(NG_ANIMATE_STATE) || {}; - var classes = resolveElementClasses(element, cache, state.active); - return !classes - ? done() - : performAnimation('setClass', classes, element, parentElement, null, function() { - if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]); - if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]); - }, cache.options, done); - }); - }, + function executeAnimationFn(fn, element, event, options, onDone) { + var args; + switch (event) { + case 'animate': + args = [element, options.from, options.to, onDone]; + break; - /** - * @ngdoc method - * @name $animate#cancel - * @kind function - * - * @param {Promise} animationPromise The animation promise that is returned when an animation is started. - * - * @description - * Cancels the provided animation. - */ - cancel: function(promise) { - promise.$$cancelFn(); - }, + case 'setClass': + args = [element, classesToAdd, classesToRemove, onDone]; + break; - /** - * @ngdoc method - * @name $animate#enabled - * @kind function - * - * @param {boolean=} value If provided then set the animation on or off. - * @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation - * @return {boolean} Current animation state. - * - * @description - * Globally enables/disables animations. - * - */ - enabled: function(value, element) { - switch (arguments.length) { - case 2: - if (value) { - cleanup(element); - } else { - var data = element.data(NG_ANIMATE_STATE) || {}; - data.disabled = true; - element.data(NG_ANIMATE_STATE, data); - } + case 'addClass': + args = [element, classesToAdd, onDone]; break; - case 1: - rootAnimateState.disabled = !value; + case 'removeClass': + args = [element, classesToRemove, onDone]; break; - default: - value = !rootAnimateState.disabled; + default: + args = [element, onDone]; break; - } - return !!value; - } - }; + } - /* - all animations call this shared animation triggering function internally. - The animationEvent variable refers to the JavaScript animation event that will be triggered - and the className value is the name of the animation that will be applied within the - CSS code. Element, `parentElement` and `afterElement` are provided DOM elements for the animation - and the onComplete callback will be fired once the animation is fully complete. - */ - function performAnimation(animationEvent, className, element, parentElement, afterElement, domOperation, options, doneCallback) { - var noopCancel = noop; - var runner = animationRunner(element, animationEvent, className, options); - if (!runner) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - closeAnimation(); - return noopCancel; - } - - animationEvent = runner.event; - className = runner.className; - var elementEvents = angular.element._data(runner.node); - elementEvents = elementEvents && elementEvents.events; - - if (!parentElement) { - parentElement = afterElement ? afterElement.parent() : element.parent(); - } - - //skip the animation if animations are disabled, a parent is already being animated, - //the element is not currently attached to the document body or then completely close - //the animation if any matching animations are not found at all. - //NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case an animation was found. - if (animationsDisabled(element, parentElement)) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - closeAnimation(); - return noopCancel; - } - - var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; - var totalActiveAnimations = ngAnimateState.totalActive || 0; - var lastAnimation = ngAnimateState.last; - var skipAnimation = false; - - if (totalActiveAnimations > 0) { - var animationsToCancel = []; - if (!runner.isClassBased) { - if (animationEvent == 'leave' && runningAnimations['ng-leave']) { - skipAnimation = true; - } else { - //cancel all animations when a structural animation takes place - for (var klass in runningAnimations) { - animationsToCancel.push(runningAnimations[klass]); - } - ngAnimateState = {}; - cleanup(element, true); - } - } else if (lastAnimation.event == 'setClass') { - animationsToCancel.push(lastAnimation); - cleanup(element, className); - } else if (runningAnimations[className]) { - var current = runningAnimations[className]; - if (current.event == animationEvent) { - skipAnimation = true; - } else { - animationsToCancel.push(current); - cleanup(element, className); - } + args.push(options); + + var value = fn.apply(fn, args); + if (value) { + if (isFunction(value.start)) { + value = value.start(); } - if (animationsToCancel.length > 0) { - forEach(animationsToCancel, function(operation) { - operation.cancel(); - }); + if (value instanceof $$AnimateRunner) { + value.done(onDone); + } else if (isFunction(value)) { + // optional onEnd / onCancel callback + return value; } } - if (runner.isClassBased - && !runner.isSetClassOperation - && animationEvent != 'animate' - && !skipAnimation) { - skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR - } - - if (skipAnimation) { - fireDOMOperation(); - fireBeforeCallbackAsync(); - fireAfterCallbackAsync(); - fireDoneCallbackAsync(); - return noopCancel; - } - - runningAnimations = ngAnimateState.active || {}; - totalActiveAnimations = ngAnimateState.totalActive || 0; - - if (animationEvent == 'leave') { - //there's no need to ever remove the listener since the element - //will be removed (destroyed) after the leave animation ends or - //is cancelled midway - element.one('$destroy', function(e) { - var element = angular.element(this); - var state = element.data(NG_ANIMATE_STATE); - if (state) { - var activeLeaveAnimation = state.active['ng-leave']; - if (activeLeaveAnimation) { - activeLeaveAnimation.cancel(); - cleanup(element, 'ng-leave'); + return noop; + } + + function groupEventedAnimations(element, event, options, animations, fnName) { + var operations = []; + forEach(animations, function(ani) { + var animation = ani[fnName]; + if (!animation) return; + + // note that all of these animations will run in parallel + operations.push(function() { + var runner; + var endProgressCb; + + var resolved = false; + var onAnimationComplete = function(rejected) { + if (!resolved) { + resolved = true; + (endProgressCb || noop)(rejected); + runner.complete(!rejected); } - } - }); - } + }; - //the ng-animate class does nothing, but it's here to allow for - //parent animations to find and cancel child animations when needed - $$jqLite.addClass(element, NG_ANIMATE_CLASS_NAME); - if (options && options.tempClasses) { - forEach(options.tempClasses, function(className) { - $$jqLite.addClass(element, className); - }); - } + runner = new $$AnimateRunner({ + end: function() { + onAnimationComplete(); + }, + cancel: function() { + onAnimationComplete(true); + } + }); - var localAnimationCount = globalAnimationCounter++; - totalActiveAnimations++; - runningAnimations[className] = runner; + endProgressCb = executeAnimationFn(animation, element, event, options, function(result) { + var cancelled = result === false; + onAnimationComplete(cancelled); + }); - element.data(NG_ANIMATE_STATE, { - last: runner, - active: runningAnimations, - index: localAnimationCount, - totalActive: totalActiveAnimations + return runner; + }); }); - //first we run the before animations and when all of those are complete - //then we perform the DOM operation and run the next set of animations - fireBeforeCallbackAsync(); - runner.before(function(cancelled) { - var data = element.data(NG_ANIMATE_STATE); - cancelled = cancelled || - !data || !data.active[className] || - (runner.isClassBased && data.active[className].event != animationEvent); - - fireDOMOperation(); - if (cancelled === true) { - closeAnimation(); - } else { - fireAfterCallbackAsync(); - runner.after(closeAnimation); + return operations; + } + + function packageAnimations(element, event, options, animations, fnName) { + var operations = groupEventedAnimations(element, event, options, animations, fnName); + if (operations.length === 0) { + var a,b; + if (fnName === 'beforeSetClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); + } else if (fnName === 'setClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass'); } - }); - return runner.cancel; + if (a) { + operations = operations.concat(a); + } + if (b) { + operations = operations.concat(b); + } + } - function fireDOMCallback(animationPhase) { - var eventName = '$animate:' + animationPhase; - if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { - $$asyncCallback(function() { - element.triggerHandler(eventName, { - event: animationEvent, - className: className - }); + if (operations.length === 0) return; + + // TODO(matsko): add documentation + return function startAnimation(callback) { + var runners = []; + if (operations.length) { + forEach(operations, function(animateFn) { + runners.push(animateFn()); }); } - } - function fireBeforeCallbackAsync() { - fireDOMCallback('before'); - } + runners.length ? $$AnimateRunner.all(runners, callback) : callback(); - function fireAfterCallbackAsync() { - fireDOMCallback('after'); - } + return function endFn(reject) { + forEach(runners, function(runner) { + reject ? runner.cancel() : runner.end(); + }); + }; + }; + } + }; - function fireDoneCallbackAsync() { - fireDOMCallback('close'); - doneCallback(); + function lookupAnimations(classes) { + classes = isArray(classes) ? classes : classes.split(' '); + var matches = [], flagMap = {}; + for (var i=0; i < classes.length; i++) { + var klass = classes[i], + animationFactory = $animateProvider.$$registeredAnimations[klass]; + if (animationFactory && !flagMap[klass]) { + matches.push($injector.get(animationFactory)); + flagMap[klass] = true; } + } + return matches; + } + }]; +}]; + +var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateJsDriver'); + this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { + return function initDriverFn(animationDetails) { + if (animationDetails.from && animationDetails.to) { + var fromAnimation = prepareAnimation(animationDetails.from); + var toAnimation = prepareAnimation(animationDetails.to); + if (!fromAnimation && !toAnimation) return; - //it is less complicated to use a flag than managing and canceling - //timeouts containing multiple callbacks. - function fireDOMOperation() { - if (!fireDOMOperation.hasBeenRun) { - fireDOMOperation.hasBeenRun = true; - domOperation(); - } - } + return { + start: function() { + var animationRunners = []; - function closeAnimation() { - if (!closeAnimation.hasBeenRun) { - if (runner) { //the runner doesn't exist if it fails to instantiate - runner.applyStyles(); + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); } - closeAnimation.hasBeenRun = true; - if (options && options.tempClasses) { - forEach(options.tempClasses, function(className) { - $$jqLite.removeClass(element, className); - }); + if (toAnimation) { + animationRunners.push(toAnimation.start()); } - var data = element.data(NG_ANIMATE_STATE); - if (data) { + $$AnimateRunner.all(animationRunners, done); - /* only structural animations wait for reflow before removing an - animation, but class-based animations don't. An example of this - failing would be when a parent HTML tag has a ng-class attribute - causing ALL directives below to skip animations during the digest */ - if (runner && runner.isClassBased) { - cleanup(element, className); - } else { - $$asyncCallback(function() { - var data = element.data(NG_ANIMATE_STATE) || {}; - if (localAnimationCount == data.index) { - cleanup(element, className, animationEvent); - } + var runner = new $$AnimateRunner({ + end: endFnFactory(), + cancel: endFnFactory() + }); + + return runner; + + function endFnFactory() { + return function() { + forEach(animationRunners, function(runner) { + // at this point we cannot cancel animations for groups just yet. 1.5+ + runner.end(); }); - element.data(NG_ANIMATE_STATE, data); - } + }; + } + + function done(status) { + runner.complete(status); } - fireDoneCallbackAsync(); } - } + }; + } else { + return prepareAnimation(animationDetails); } + }; - function cancelChildAnimations(element) { - var node = extractElementNode(element); - if (node) { - var nodes = angular.isFunction(node.getElementsByClassName) ? - node.getElementsByClassName(NG_ANIMATE_CLASS_NAME) : - node.querySelectorAll('.' + NG_ANIMATE_CLASS_NAME); - forEach(nodes, function(element) { - element = angular.element(element); - var data = element.data(NG_ANIMATE_STATE); - if (data && data.active) { - forEach(data.active, function(runner) { - runner.cancel(); - }); - } + function prepareAnimation(animationDetails) { + // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations + var element = animationDetails.element; + var event = animationDetails.event; + var options = animationDetails.options; + var classes = animationDetails.classes; + return $$animateJs(element, event, classes, options); + } + }]; +}]; + +var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; +var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; +var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { + var PRE_DIGEST_STATE = 1; + var RUNNING_STATE = 2; + var ONE_SPACE = ' '; + + var rules = this.rules = { + skip: [], + cancel: [], + join: [] + }; + + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + return rules[ruleType].some(function(fn) { + return fn(element, currentAnimation, previousAnimation); + }); + } + + function hasAnimationClasses(animation, and) { + var a = (animation.addClass || '').length > 0; + var b = (animation.removeClass || '').length > 0; + return and ? a && b : a || b; + } + + rules.join.push(function(element, newAnimation, currentAnimation) { + // if the new animation is class-based then we can just tack that on + return !newAnimation.structural && hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // there is no need to animate anything if no classes are being added and + // there is no structural animation that will be triggered + return !newAnimation.structural && !hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // why should we trigger a new structural animation if the element will + // be removed from the DOM anyway? + return currentAnimation.event == 'leave' && newAnimation.structural; + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // if there is an ongoing current animation then don't even bother running the class-based animation + return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // there can never be two structural animations running at the same time + return currentAnimation.structural && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // if the previous animation is already running, but the new animation will + // be triggered, but the new animation is structural + return currentAnimation.state === RUNNING_STATE && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + var nA = newAnimation.addClass; + var nR = newAnimation.removeClass; + var cA = currentAnimation.addClass; + var cR = currentAnimation.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } + + return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); + }); + + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + function($$rAF, $rootScope, $rootElement, $document, $$HashMap, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + + var activeAnimationsLookup = new $$HashMap(); + var disabledElementsLookup = new $$HashMap(); + var animationsEnabled = null; + + function postDigestTaskFactory() { + var postDigestCalled = false; + return function(fn) { + // we only issue a call to postDigest before + // it has first passed. This prevents any callbacks + // from not firing once the animation has completed + // since it will be out of the digest cycle. + if (postDigestCalled) { + fn(); + } else { + $rootScope.$$postDigest(function() { + postDigestCalled = true; + fn(); }); } + }; + } + + // Wait until all directive and route-related templates are downloaded and + // compiled. The $templateRequest.totalPendingRequests variable keeps track of + // all of the remote templates being currently downloaded. If there are no + // templates currently downloading then the watcher will still fire anyway. + var deregisterWatch = $rootScope.$watch( + function() { return $templateRequest.totalPendingRequests === 0; }, + function(isEmpty) { + if (!isEmpty) return; + deregisterWatch(); + + // Now that all templates have been downloaded, $animate will wait until + // the post digest queue is empty before enabling animations. By having two + // calls to $postDigest calls we can ensure that the flag is enabled at the + // very end of the post digest queue. Since all of the animations in $animate + // use $postDigest, it's important that the code below executes at the end. + // This basically means that the page is fully downloaded and compiled before + // any animations are triggered. + $rootScope.$$postDigest(function() { + $rootScope.$$postDigest(function() { + // we check for null directly in the event that the application already called + // .enabled() with whatever arguments that it provided it with + if (animationsEnabled === null) { + animationsEnabled = true; + } + }); + }); } + ); - function cleanup(element, className) { - if (isMatchingElement(element, $rootElement)) { - if (!rootAnimateState.disabled) { - rootAnimateState.running = false; - rootAnimateState.structural = false; - } - } else if (className) { - var data = element.data(NG_ANIMATE_STATE) || {}; + var callbackRegistry = {}; - var removeAnimations = className === true; - if (!removeAnimations && data.active && data.active[className]) { - data.totalActive--; - delete data.active[className]; - } + // remember that the classNameFilter is set during the provider/config + // stage therefore we can optimize here and setup a helper function + var classNameFilter = $animateProvider.classNameFilter(); + var isAnimatableClassName = !classNameFilter + ? function() { return true; } + : function(className) { + return classNameFilter.test(className); + }; + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); - if (removeAnimations || !data.totalActive) { - $$jqLite.removeClass(element, NG_ANIMATE_CLASS_NAME); - element.removeData(NG_ANIMATE_STATE); + function normalizeAnimationDetails(element, animation) { + return mergeAnimationDetails(element, animation, {}); + } + + // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var contains = Node.prototype.contains || function(arg) { + // jshint bitwise: false + return this === arg || !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true + }; + + function findCallbacks(parent, element, event) { + var targetNode = getDomNode(element); + var targetParentNode = getDomNode(parent); + + var matches = []; + var entries = callbackRegistry[event]; + if (entries) { + forEach(entries, function(entry) { + if (contains.call(entry.node, targetNode)) { + matches.push(entry.callback); + } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { + matches.push(entry.callback); } - } + }); } - function animationsDisabled(element, parentElement) { - if (rootAnimateState.disabled) { - return true; - } + return matches; + } - if (isMatchingElement(element, $rootElement)) { - return rootAnimateState.running; + return { + on: function(event, container, callback) { + var node = extractElementNode(container); + callbackRegistry[event] = callbackRegistry[event] || []; + callbackRegistry[event].push({ + node: node, + callback: callback + }); + }, + + off: function(event, container, callback) { + var entries = callbackRegistry[event]; + if (!entries) return; + + callbackRegistry[event] = arguments.length === 1 + ? null + : filterFromRegistry(entries, container, callback); + + function filterFromRegistry(list, matchContainer, matchCallback) { + var containerNode = extractElementNode(matchContainer); + return list.filter(function(entry) { + var isMatch = entry.node === containerNode && + (!matchCallback || entry.callback === matchCallback); + return !isMatch; + }); } + }, + + pin: function(element, parentElement) { + assertArg(isElement(element), 'element', 'not an element'); + assertArg(isElement(parentElement), 'parentElement', 'not an element'); + element.data(NG_ANIMATE_PIN_DATA, parentElement); + }, + + push: function(element, event, options, domOperation) { + options = options || {}; + options.domOperation = domOperation; + return queueAnimation(element, event, options); + }, + + // this method has four signatures: + // () - global getter + // (bool) - global setter + // (element) - element getter + // (element, bool) - element setter<F37> + enabled: function(element, bool) { + var argCount = arguments.length; + + if (argCount === 0) { + // () - Global getter + bool = !!animationsEnabled; + } else { + var hasElement = isElement(element); - var allowChildAnimations, parentRunningAnimation, hasParent; - do { - //the element did not reach the root element which means that it - //is not apart of the DOM. Therefore there is no reason to do - //any animations on it - if (parentElement.length === 0) break; - - var isRoot = isMatchingElement(parentElement, $rootElement); - var state = isRoot ? rootAnimateState : (parentElement.data(NG_ANIMATE_STATE) || {}); - if (state.disabled) { - return true; - } - - //no matter what, for an animation to work it must reach the root element - //this implies that the element is attached to the DOM when the animation is run - if (isRoot) { - hasParent = true; - } + if (!hasElement) { + // (bool) - Global setter + bool = animationsEnabled = !!element; + } else { + var node = getDomNode(element); + var recordExists = disabledElementsLookup.get(node); - //once a flag is found that is strictly false then everything before - //it will be discarded and all child animations will be restricted - if (allowChildAnimations !== false) { - var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN); - if (angular.isDefined(animateChildrenFlag)) { - allowChildAnimations = animateChildrenFlag; + if (argCount === 1) { + // (element) - Element getter + bool = !recordExists; + } else { + // (element, bool) - Element setter + disabledElementsLookup.put(node, !bool); } } + } - parentRunningAnimation = parentRunningAnimation || - state.running || - (state.last && !state.last.isClassBased); - } - while (parentElement = parentElement.parent()); - - return !hasParent || (!allowChildAnimations && parentRunningAnimation); - } - }]); - - $animateProvider.register('', ['$window', '$sniffer', '$timeout', '$$animateReflow', - function($window, $sniffer, $timeout, $$animateReflow) { - // Detect proper transitionend/animationend event names. - var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; - - // If unprefixed events are not supported but webkit-prefixed are, use the latter. - // Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. - // Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` - // but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. - // Register both events in case `window.onanimationend` is not supported because of that, - // do the same for `transitionend` as Safari is likely to exhibit similar behavior. - // Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit - // therefore there is no reason to test anymore for other vendor prefixes: http://caniuse.com/#search=transition - if (window.ontransitionend === undefined && window.onwebkittransitionend !== undefined) { - CSS_PREFIX = '-webkit-'; - TRANSITION_PROP = 'WebkitTransition'; - TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; - } else { - TRANSITION_PROP = 'transition'; - TRANSITIONEND_EVENT = 'transitionend'; + return bool; } + }; - if (window.onanimationend === undefined && window.onwebkitanimationend !== undefined) { - CSS_PREFIX = '-webkit-'; - ANIMATION_PROP = 'WebkitAnimation'; - ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; - } else { - ANIMATION_PROP = 'animation'; - ANIMATIONEND_EVENT = 'animationend'; - } - - var DURATION_KEY = 'Duration'; - var PROPERTY_KEY = 'Property'; - var DELAY_KEY = 'Delay'; - var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; - var ANIMATION_PLAYSTATE_KEY = 'PlayState'; - var NG_ANIMATE_PARENT_KEY = '$$ngAnimateKey'; - var NG_ANIMATE_CSS_DATA_KEY = '$$ngAnimateCSS3Data'; - var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; - var CLOSING_TIME_BUFFER = 1.5; - var ONE_SECOND = 1000; - - var lookupCache = {}; - var parentCounter = 0; - var animationReflowQueue = []; - var cancelAnimationReflow; - function clearCacheAfterReflow() { - if (!cancelAnimationReflow) { - cancelAnimationReflow = $$animateReflow(function() { - animationReflowQueue = []; - cancelAnimationReflow = null; - lookupCache = {}; - }); - } + function queueAnimation(element, event, initialOptions) { + // we always make a copy of the options since + // there should never be any side effects on + // the input data when running `$animateCss`. + var options = copy(initialOptions); + + var node, parent; + element = stripCommentsFromElement(element); + if (element) { + node = getDomNode(element); + parent = element.parent(); } - function afterReflow(element, callback) { - if (cancelAnimationReflow) { - cancelAnimationReflow(); - } - animationReflowQueue.push(callback); - cancelAnimationReflow = $$animateReflow(function() { - forEach(animationReflowQueue, function(fn) { - fn(); - }); + options = prepareAnimationOptions(options); - animationReflowQueue = []; - cancelAnimationReflow = null; - lookupCache = {}; - }); + // we create a fake runner with a working promise. + // These methods will become available after the digest has passed + var runner = new $$AnimateRunner(); + + // this is used to trigger callbacks in postDigest mode + var runInNextPostDigestOrNow = postDigestTaskFactory(); + + if (isArray(options.addClass)) { + options.addClass = options.addClass.join(' '); } - var closingTimer = null; - var closingTimestamp = 0; - var animationElementQueue = []; - function animationCloseHandler(element, totalTime) { - var node = extractElementNode(element); - element = angular.element(node); + if (options.addClass && !isString(options.addClass)) { + options.addClass = null; + } - //this item will be garbage collected by the closing - //animation timeout - animationElementQueue.push(element); + if (isArray(options.removeClass)) { + options.removeClass = options.removeClass.join(' '); + } - //but it may not need to cancel out the existing timeout - //if the timestamp is less than the previous one - var futureTimestamp = Date.now() + totalTime; - if (futureTimestamp <= closingTimestamp) { - return; - } + if (options.removeClass && !isString(options.removeClass)) { + options.removeClass = null; + } + + if (options.from && !isObject(options.from)) { + options.from = null; + } - $timeout.cancel(closingTimer); + if (options.to && !isObject(options.to)) { + options.to = null; + } - closingTimestamp = futureTimestamp; - closingTimer = $timeout(function() { - closeAllAnimations(animationElementQueue); - animationElementQueue = []; - }, totalTime, false); + // there are situations where a directive issues an animation for + // a jqLite wrapper that contains only comment nodes... If this + // happens then there is no way we can perform an animation + if (!node) { + close(); + return runner; } - function closeAllAnimations(elements) { - forEach(elements, function(element) { - var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if (elementData) { - forEach(elementData.closeAnimationFns, function(fn) { - fn(); - }); - } - }); + var className = [node.className, options.addClass, options.removeClass].join(' '); + if (!isAnimatableClassName(className)) { + close(); + return runner; } - function getElementAnimationDetails(element, cacheKey) { - var data = cacheKey ? lookupCache[cacheKey] : null; - if (!data) { - var transitionDuration = 0; - var transitionDelay = 0; - var animationDuration = 0; - var animationDelay = 0; + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // this is a hard disable of all animations for the application or on + // the element itself, therefore there is no need to continue further + // past this point if not enabled + // Animations are also disabled if the document is currently hidden (page is not visible + // to the user), because browsers slow down or do not flush calls to requestAnimationFrame + var skipAnimations = !animationsEnabled || $document[0].hidden || disabledElementsLookup.get(node); + var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; + var hasExistingAnimation = !!existingAnimation.state; + + // there is no point in traversing the same collection of parent ancestors if a followup + // animation will be run on the same element that already did all that checking work + if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(element, parent, event); + } + + if (skipAnimations) { + close(); + return runner; + } - //we want all the styles defined before and after - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; + if (isStructural) { + closeChildAnimations(element); + } - var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); + var newAnimation = { + structural: isStructural, + element: element, + event: event, + addClass: options.addClass, + removeClass: options.removeClass, + close: close, + options: options, + runner: runner + }; - var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); + if (hasExistingAnimation) { + var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + if (skipAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + close(); + return runner; + } else { + mergeAnimationDetails(element, existingAnimation, newAnimation); + return existingAnimation.runner; + } + } + var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + if (cancelAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + // this will end the animation right away and it is safe + // to do so since the animation is already running and the + // runner callback code will run in async + existingAnimation.runner.end(); + } else if (existingAnimation.structural) { + // this means that the animation is queued into a digest, but + // hasn't started yet. Therefore it is safe to run the close + // method which will call the runner methods in async. + existingAnimation.close(); + } else { + // this will merge the new animation options into existing animation options + mergeAnimationDetails(element, existingAnimation, newAnimation); - var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; - animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay); + return existingAnimation.runner; + } + } else { + // a joined animation means that this animation will take over the existing one + // so an example would involve a leave animation taking over an enter. Then when + // the postDigest kicks in the enter will be ignored. + var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); + if (joinAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + normalizeAnimationDetails(element, newAnimation); + } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); - var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); + event = newAnimation.event = existingAnimation.event; + options = mergeAnimationDetails(element, existingAnimation, newAnimation); - if (aDuration > 0) { - aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; - } - animationDuration = Math.max(aDuration, animationDuration); + //we return the same runner since only the option values of this animation will + //be fed into the `existingAnimation`. + return existingAnimation.runner; } - }); - data = { - total: 0, - transitionDelay: transitionDelay, - transitionDuration: transitionDuration, - animationDelay: animationDelay, - animationDuration: animationDuration - }; - if (cacheKey) { - lookupCache[cacheKey] = data; } } - return data; + } else { + // normalization in this case means that it removes redundant CSS classes that + // already exist (addClass) or do not exist (removeClass) on the element + normalizeAnimationDetails(element, newAnimation); } - function parseMaxTime(str) { - var maxValue = 0; - var values = isString(str) ? - str.split(/\s*,\s*/) : - []; - forEach(values, function(value) { - maxValue = Math.max(parseFloat(value) || 0, maxValue); - }); - return maxValue; + // when the options are merged and cleaned up we may end up not having to do + // an animation at all, therefore we should check this before issuing a post + // digest callback. Structural animations will always run no matter what. + var isValidAnimation = newAnimation.structural; + if (!isValidAnimation) { + // animate (from/to) can be quickly checked first, otherwise we check if any classes are present + isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) + || hasAnimationClasses(newAnimation); } - function getCacheKey(element) { - var parentElement = element.parent(); - var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); - if (!parentID) { - parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); - parentID = parentCounter; - } - return parentID + '-' + extractElementNode(element).getAttribute('class'); + if (!isValidAnimation) { + close(); + clearElementAnimationState(element); + return runner; } - function animateSetup(animationEvent, element, className, styles) { - var structural = ['ng-enter','ng-leave','ng-move'].indexOf(className) >= 0; - - var cacheKey = getCacheKey(element); - var eventCacheKey = cacheKey + ' ' + className; - var itemIndex = lookupCache[eventCacheKey] ? ++lookupCache[eventCacheKey].total : 0; - - var stagger = {}; - if (itemIndex > 0) { - var staggerClassName = className + '-stagger'; - var staggerCacheKey = cacheKey + ' ' + staggerClassName; - var applyClasses = !lookupCache[staggerCacheKey]; + // the counter keeps track of cancelled animations + var counter = (existingAnimation.counter || 0) + 1; + newAnimation.counter = counter; + + markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + + $rootScope.$$postDigest(function() { + var animationDetails = activeAnimationsLookup.get(node); + var animationCancelled = !animationDetails; + animationDetails = animationDetails || {}; + + // if addClass/removeClass is called before something like enter then the + // registered parent element may not be present. The code below will ensure + // that a final value for parent element is obtained + var parentElement = element.parent() || []; + + // animate/structural/class-based animations all have requirements. Otherwise there + // is no point in performing an animation. The parent node must also be set. + var isValidAnimation = parentElement.length > 0 + && (animationDetails.event === 'animate' + || animationDetails.structural + || hasAnimationClasses(animationDetails)); + + // this means that the previous animation was cancelled + // even if the follow-up animation is the same event + if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) { + // if another animation did not take over then we need + // to make sure that the domOperation and options are + // handled accordingly + if (animationCancelled) { + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + } - applyClasses && $$jqLite.addClass(element, staggerClassName); + // if the event changed from something like enter to leave then we do + // it, otherwise if it's the same then the end result will be the same too + if (animationCancelled || (isStructural && animationDetails.event !== event)) { + options.domOperation(); + runner.end(); + } - stagger = getElementAnimationDetails(element, staggerCacheKey); + // in the event that the element animation was not cancelled or a follow-up animation + // isn't allowed to animate from here then we need to clear the state of the element + // so that any future animations won't read the expired animation data. + if (!isValidAnimation) { + clearElementAnimationState(element); + } - applyClasses && $$jqLite.removeClass(element, staggerClassName); + return; } - $$jqLite.addClass(element, className); + // this combined multiple class to addClass / removeClass into a setClass event + // so long as a structural event did not take over the animation + event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) + ? 'setClass' + : animationDetails.event; - var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; - var timings = getElementAnimationDetails(element, eventCacheKey); - var transitionDuration = timings.transitionDuration; - var animationDuration = timings.animationDuration; + markElementAnimationState(element, RUNNING_STATE); + var realRunner = $$animation(element, event, animationDetails.options); - if (structural && transitionDuration === 0 && animationDuration === 0) { - $$jqLite.removeClass(element, className); - return false; - } + realRunner.done(function(status) { + close(!status); + var animationDetails = activeAnimationsLookup.get(node); + if (animationDetails && animationDetails.counter === counter) { + clearElementAnimationState(getDomNode(element)); + } + notifyProgress(runner, event, 'close', {}); + }); - var blockTransition = styles || (structural && transitionDuration > 0); - var blockAnimation = animationDuration > 0 && - stagger.animationDelay > 0 && - stagger.animationDuration === 0; + // this will update the runner's flow-control events based on + // the `realRunner` object. + runner.setHost(realRunner); + notifyProgress(runner, event, 'start', {}); + }); - var closeAnimationFns = formerData.closeAnimationFns || []; - element.data(NG_ANIMATE_CSS_DATA_KEY, { - stagger: stagger, - cacheKey: eventCacheKey, - running: formerData.running || 0, - itemIndex: itemIndex, - blockTransition: blockTransition, - closeAnimationFns: closeAnimationFns + return runner; + + function notifyProgress(runner, event, phase, data) { + runInNextPostDigestOrNow(function() { + var callbacks = findCallbacks(parent, element, event); + if (callbacks.length) { + // do not optimize this call here to RAF because + // we don't know how heavy the callback code here will + // be and if this code is buffered then this can + // lead to a performance regression. + $$rAF(function() { + forEach(callbacks, function(callback) { + callback(element, phase, data); + }); + }); + } }); + runner.progress(event, phase, data); + } - var node = extractElementNode(element); + function close(reject) { // jshint ignore:line + clearGeneratedClasses(element, options); + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + runner.complete(!reject); + } + } - if (blockTransition) { - blockTransitions(node, true); - if (styles) { - element.css(styles); + function closeChildAnimations(element) { + var node = getDomNode(element); + var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); + forEach(children, function(child) { + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var animationDetails = activeAnimationsLookup.get(child); + if (animationDetails) { + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: + activeAnimationsLookup.remove(child); + break; } } + }); + } - if (blockAnimation) { - blockAnimations(node, true); - } + function clearElementAnimationState(element) { + var node = getDomNode(element); + node.removeAttribute(NG_ANIMATE_ATTR_NAME); + activeAnimationsLookup.remove(node); + } + + function isMatchingElement(nodeOrElmA, nodeOrElmB) { + return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); + } - return true; + /** + * This fn returns false if any of the following is true: + * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed + * b) a parent element has an ongoing structural animation, and animateChildren is false + * c) the element is not a child of the body + * d) the element is not a child of the $rootElement + */ + function areAnimationsAllowed(element, parentElement, event) { + var bodyElement = jqLite($document[0].body); + var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); + var parentAnimationDetected = false; + var animateChildren; + var elementDisabled = disabledElementsLookup.get(getDomNode(element)); + + var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + if (parentHost) { + parentElement = parentHost; } - function animateRun(animationEvent, element, className, activeAnimationComplete, styles) { - var node = extractElementNode(element); - var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if (node.getAttribute('class').indexOf(className) == -1 || !elementData) { - activeAnimationComplete(); - return; + parentElement = getDomNode(parentElement); + + while (parentElement) { + if (!rootElementDetected) { + // angular doesn't want to attempt to animate elements outside of the application + // therefore we need to ensure that the rootElement is an ancestor of the current element + rootElementDetected = isMatchingElement(parentElement, $rootElement); } - var activeClassName = ''; - var pendingClassName = ''; - forEach(className.split(' '), function(klass, i) { - var prefix = (i > 0 ? ' ' : '') + klass; - activeClassName += prefix + '-active'; - pendingClassName += prefix + '-pending'; - }); + if (parentElement.nodeType !== ELEMENT_NODE) { + // no point in inspecting the #document element + break; + } - var style = ''; - var appliedStyles = []; - var itemIndex = elementData.itemIndex; - var stagger = elementData.stagger; - var staggerTime = 0; - if (itemIndex > 0) { - var transitionStaggerDelay = 0; - if (stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { - transitionStaggerDelay = stagger.transitionDelay * itemIndex; + var details = activeAnimationsLookup.get(parentElement) || {}; + // either an enter, leave or move animation will commence + // therefore we can't allow any animations to take place + // but if a parent animation is class-based then that's ok + if (!parentAnimationDetected) { + var parentElementDisabled = disabledElementsLookup.get(parentElement); + + if (parentElementDisabled === true && elementDisabled !== false) { + // disable animations if the user hasn't explicitly enabled animations on the + // current element + elementDisabled = true; + // element is disabled via parent element, no need to check anything else + break; + } else if (parentElementDisabled === false) { + elementDisabled = false; } + parentAnimationDetected = details.structural; + } - var animationStaggerDelay = 0; - if (stagger.animationDelay > 0 && stagger.animationDuration === 0) { - animationStaggerDelay = stagger.animationDelay * itemIndex; - appliedStyles.push(CSS_PREFIX + 'animation-play-state'); + if (isUndefined(animateChildren) || animateChildren === true) { + var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + if (isDefined(value)) { + animateChildren = value; } - - staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100; } - if (!staggerTime) { - $$jqLite.addClass(element, activeClassName); - if (elementData.blockTransition) { - blockTransitions(node, false); - } + // there is no need to continue traversing at this point + if (parentAnimationDetected && animateChildren === false) break; + + if (!bodyElementDetected) { + // we also need to ensure that the element is or will be a part of the body element + // otherwise it is pointless to even issue an animation to be rendered + bodyElementDetected = isMatchingElement(parentElement, bodyElement); } - var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; - var timings = getElementAnimationDetails(element, eventCacheKey); - var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); - if (maxDuration === 0) { - $$jqLite.removeClass(element, activeClassName); - animateClose(element, className); - activeAnimationComplete(); - return; + if (bodyElementDetected && rootElementDetected) { + // If both body and root have been found, any other checks are pointless, + // as no animation data should live outside the application + break; } - if (!staggerTime && styles && Object.keys(styles).length > 0) { - if (!timings.transitionDuration) { - element.css('transition', timings.animationDuration + 's linear all'); - appliedStyles.push('transition'); + if (!rootElementDetected) { + // If no rootElement is detected, check if the parentElement is pinned to another element + parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA); + if (parentHost) { + // The pin target element becomes the next parent element + parentElement = getDomNode(parentHost); + continue; } - element.css(styles); } - var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); - var maxDelayTime = maxDelay * ONE_SECOND; + parentElement = parentElement.parentNode; + } - if (appliedStyles.length > 0) { - //the element being animated may sometimes contain comment nodes in - //the jqLite object, so we're safe to use a single variable to house - //the styles since there is always only one element being animated - var oldStyle = node.getAttribute('style') || ''; - if (oldStyle.charAt(oldStyle.length - 1) !== ';') { - oldStyle += ';'; - } - node.setAttribute('style', oldStyle + ' ' + style); - } + var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; + return allowAnimation && rootElementDetected && bodyElementDetected; + } - var startTime = Date.now(); - var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; - var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; - var totalTime = (staggerTime + animationTime) * ONE_SECOND; + function markElementAnimationState(element, state, details) { + details = details || {}; + details.state = state; - var staggerTimeout; - if (staggerTime > 0) { - $$jqLite.addClass(element, pendingClassName); - staggerTimeout = $timeout(function() { - staggerTimeout = null; + var node = getDomNode(element); + node.setAttribute(NG_ANIMATE_ATTR_NAME, state); - if (timings.transitionDuration > 0) { - blockTransitions(node, false); - } - if (timings.animationDuration > 0) { - blockAnimations(node, false); - } + var oldValue = activeAnimationsLookup.get(node); + var newValue = oldValue + ? extend(oldValue, details) + : details; + activeAnimationsLookup.put(node, newValue); + } + }]; +}]; - $$jqLite.addClass(element, activeClassName); - $$jqLite.removeClass(element, pendingClassName); +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; - if (styles) { - if (timings.transitionDuration === 0) { - element.css('transition', timings.animationDuration + 's linear all'); - } - element.css(styles); - appliedStyles.push('transition'); - } - }, staggerTime * ONE_SECOND, false); - } + var drivers = this.drivers = []; + + var RUNNER_STORAGE_KEY = '$$animationRunner'; + + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } + + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); + } + + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } + + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function sortAnimations(animations) { + var tree = { children: [] }; + var i, lookup = new $$HashMap(); - element.on(css3AnimationEvents, onAnimationProgress); - elementData.closeAnimationFns.push(function() { - onEnd(); - activeAnimationComplete(); + // this is done first beforehand so that the hashmap + // is filled with a list of the elements that will be animated + for (i = 0; i < animations.length; i++) { + var animation = animations[i]; + lookup.put(animation.domNode, animations[i] = { + domNode: animation.domNode, + fn: animation.fn, + children: [] }); + } - elementData.running++; - animationCloseHandler(element, totalTime); - return onEnd; - - // This will automatically be called by $animate so - // there is no need to attach this internally to the - // timeout done method. - function onEnd() { - element.off(css3AnimationEvents, onAnimationProgress); - $$jqLite.removeClass(element, activeClassName); - $$jqLite.removeClass(element, pendingClassName); - if (staggerTimeout) { - $timeout.cancel(staggerTimeout); - } - animateClose(element, className); - var node = extractElementNode(element); - for (var i in appliedStyles) { - node.style.removeProperty(appliedStyles[i]); + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); + } + + return flatten(tree); + + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; + + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); + + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; } + parentNode = parentNode.parentNode; } - function onAnimationProgress(event) { - event.stopPropagation(); - var ev = event.originalEvent || event; - var timeStamp = ev.$manualTimeStamp || ev.timeStamp || Date.now(); - - /* Firefox (or possibly just Gecko) likes to not round values up - * when a ms measurement is used for the animation */ - var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); - - /* $manualTimeStamp is a mocked timeStamp value which is set - * within browserTrigger(). This is only here so that tests can - * mock animations properly. Real events fallback to event.timeStamp, - * or, if they don't, then a timeStamp is automatically created for them. - * We're checking to see if the timeStamp surpasses the expected delay, - * but we're using elapsedTime instead of the timeStamp on the 2nd - * pre-condition since animations sometimes close off early */ - if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { - activeAnimationComplete(); + (parentEntry || tree).children.push(entry); + return entry; + } + + function flatten(tree) { + var result = []; + var queue = []; + var i; + + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } + + var remainingLevelEntries = queue.length; + var nextLevelEntries = 0; + var row = []; + + for (i = 0; i < queue.length; i++) { + var entry = queue[i]; + if (remainingLevelEntries <= 0) { + remainingLevelEntries = nextLevelEntries; + nextLevelEntries = 0; + result.push(row); + row = []; } + row.push(entry.fn); + entry.children.forEach(function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } + + if (row.length) { + result.push(row); } - } - function blockTransitions(node, bool) { - node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : ''; + return result; } + } + + // TODO(matsko): document the signature in a better way + return function(element, event, options) { + options = prepareAnimationOptions(options); + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // there is no animation at the current moment, however + // these runner methods will get later updated with the + // methods leading into the driver's end/cancel methods + // for now they just stop the animation from starting + var runner = new $$AnimateRunner({ + end: function() { close(); }, + cancel: function() { close(true); } + }); - function blockAnimations(node, bool) { - node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : ''; + if (!drivers.length) { + close(); + return runner; } - function animateBefore(animationEvent, element, className, styles) { - if (animateSetup(animationEvent, element, className, styles)) { - return function(cancelled) { - cancelled && animateClose(element, className); - }; - } + setRunner(element, runner); + + var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); + var tempClasses = options.tempClasses; + if (tempClasses) { + classes += ' ' + tempClasses; + options.tempClasses = null; } - function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) { - if (element.data(NG_ANIMATE_CSS_DATA_KEY)) { - return animateRun(animationEvent, element, className, afterAnimationComplete, styles); - } else { - animateClose(element, className); - afterAnimationComplete(); - } + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); } - function animate(animationEvent, element, className, animationComplete, options) { - //If the animateSetup function doesn't bother returning a - //cancellation function then it means that there is no animation - //to perform at all - var preReflowCancellation = animateBefore(animationEvent, element, className, options.from); - if (!preReflowCancellation) { - clearCacheAfterReflow(); - animationComplete(); - return; - } + animationQueue.push({ + // this data is used by the postDigest code and passed into + // the driver step function + element: element, + classes: classes, + event: event, + structural: isStructural, + options: options, + beforeStart: beforeStart, + close: close + }); - //There are two cancellation functions: one is before the first - //reflow animation and the second is during the active state - //animation. The first function will take care of removing the - //data from the element which will not make the 2nd animation - //happen in the first place - var cancel = preReflowCancellation; - afterReflow(element, function() { - //once the reflow is complete then we point cancel to - //the new cancellation function which will remove all of the - //animation properties from the active animation - cancel = animateAfter(animationEvent, element, className, animationComplete, options.to); + element.on('$destroy', handleDestroyedElement); + + // we only want there to be one function called within the post digest + // block. This way we can group animations for all the animations that + // were apart of the same postDigest flush call. + if (animationQueue.length > 1) return runner; + + $rootScope.$$postDigest(function() { + var animations = []; + forEach(animationQueue, function(entry) { + // the element was destroyed early on which removed the runner + // form its storage. This means we can't animate this element + // at all and it already has been closed due to destruction. + if (getRunner(entry.element)) { + animations.push(entry); + } else { + entry.close(); + } }); - return function(cancelled) { - (cancel || noop)(cancelled); - }; + // now any future animations will be in another postDigest + animationQueue.length = 0; + + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; + + forEach(groupedAnimations, function(animationEntry) { + toBeSortedAnimations.push({ + domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + fn: function triggerAnimationStart() { + // it's important that we apply the `ng-animate` CSS class and the + // temporary classes before we do any driver invoking since these + // CSS classes may be required for proper CSS detection. + animationEntry.beforeStart(); + + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that the element was removed before the digest runs or + // during the RAF sequencing then we should not trigger the animation. + var targetElement = animationEntry.anchors + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; + } + } + + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } + } + }); + }); + + // we need to sort each of the animations in order of parent to child + // relationships. This ensures that the child classes are applied at the + // right time. + $$rAFScheduler(sortAnimations(toBeSortedAnimations)); + }); + + return runner; + + // TODO(matsko): change to reference nodes + function getAnchorNodes(node) { + var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']'; + var items = node.hasAttribute(NG_ANIMATE_REF_ATTR) + ? [node] + : node.querySelectorAll(SELECTOR); + var anchors = []; + forEach(items, function(node) { + var attr = node.getAttribute(NG_ANIMATE_REF_ATTR); + if (attr && attr.length) { + anchors.push(node); + } + }); + return anchors; } - function animateClose(element, className) { - $$jqLite.removeClass(element, className); - var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if (data) { - if (data.running) { - data.running--; + function groupAnimations(animations) { + var preparedAnimations = []; + var refLookup = {}; + forEach(animations, function(animation, index) { + var element = animation.element; + var node = getDomNode(element); + var event = animation.event; + var enterOrMove = ['enter', 'move'].indexOf(event) >= 0; + var anchorNodes = animation.structural ? getAnchorNodes(node) : []; + + if (anchorNodes.length) { + var direction = enterOrMove ? 'to' : 'from'; + + forEach(anchorNodes, function(anchor) { + var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR); + refLookup[key] = refLookup[key] || {}; + refLookup[key][direction] = { + animationID: index, + element: jqLite(anchor) + }; + }); + } else { + preparedAnimations.push(animation); } - if (!data.running || data.running === 0) { - element.removeData(NG_ANIMATE_CSS_DATA_KEY); + }); + + var usedIndicesLookup = {}; + var anchorGroups = {}; + forEach(refLookup, function(operations, key) { + var from = operations.from; + var to = operations.to; + + if (!from || !to) { + // only one of these is set therefore we can't have an + // anchor animation since all three pieces are required + var index = from ? from.animationID : to.animationID; + var indexKey = index.toString(); + if (!usedIndicesLookup[indexKey]) { + usedIndicesLookup[indexKey] = true; + preparedAnimations.push(animations[index]); + } + return; } - } - } - return { - animate: function(element, className, from, to, animationCompleted, options) { - options = options || {}; - options.from = from; - options.to = to; - return animate('animate', element, className, animationCompleted, options); - }, + var fromAnimation = animations[from.animationID]; + var toAnimation = animations[to.animationID]; + var lookupKey = from.animationID.toString(); + if (!anchorGroups[lookupKey]) { + var group = anchorGroups[lookupKey] = { + structural: true, + beforeStart: function() { + fromAnimation.beforeStart(); + toAnimation.beforeStart(); + }, + close: function() { + fromAnimation.close(); + toAnimation.close(); + }, + classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes), + from: fromAnimation, + to: toAnimation, + anchors: [] // TODO(matsko): change to reference nodes + }; - enter: function(element, animationCompleted, options) { - options = options || {}; - return animate('enter', element, 'ng-enter', animationCompleted, options); - }, + // the anchor animations require that the from and to elements both have at least + // one shared CSS class which effictively marries the two elements together to use + // the same animation driver and to properly sequence the anchor animation. + if (group.classes.length) { + preparedAnimations.push(group); + } else { + preparedAnimations.push(fromAnimation); + preparedAnimations.push(toAnimation); + } + } - leave: function(element, animationCompleted, options) { - options = options || {}; - return animate('leave', element, 'ng-leave', animationCompleted, options); - }, + anchorGroups[lookupKey].anchors.push({ + 'out': from.element, 'in': to.element + }); + }); - move: function(element, animationCompleted, options) { - options = options || {}; - return animate('move', element, 'ng-move', animationCompleted, options); - }, + return preparedAnimations; + } - beforeSetClass: function(element, add, remove, animationCompleted, options) { - options = options || {}; - var className = suffixClasses(remove, '-remove') + ' ' + - suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className, options.from); - if (cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; - } - clearCacheAfterReflow(); - animationCompleted(); - }, + function cssClassesIntersection(a,b) { + a = a.split(' '); + b = b.split(' '); + var matches = []; + + for (var i = 0; i < a.length; i++) { + var aa = a[i]; + if (aa.substring(0,3) === 'ng-') continue; - beforeAddClass: function(element, className, animationCompleted, options) { - options = options || {}; - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from); - if (cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; + for (var j = 0; j < b.length; j++) { + if (aa === b[j]) { + matches.push(aa); + break; + } } - clearCacheAfterReflow(); - animationCompleted(); - }, + } + + return matches.join(' '); + } - beforeRemoveClass: function(element, className, animationCompleted, options) { - options = options || {}; - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from); - if (cancellationMethod) { - afterReflow(element, animationCompleted); - return cancellationMethod; + function invokeFirstDriver(animationDetails) { + // we loop in reverse order since the more general drivers (like CSS and JS) + // may attempt more elements, but custom drivers are more particular + for (var i = drivers.length - 1; i >= 0; i--) { + var driverName = drivers[i]; + if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check + + var factory = $injector.get(driverName); + var driver = factory(animationDetails); + if (driver) { + return driver; } - clearCacheAfterReflow(); - animationCompleted(); - }, + } + } - setClass: function(element, add, remove, animationCompleted, options) { - options = options || {}; - remove = suffixClasses(remove, '-remove'); - add = suffixClasses(add, '-add'); - var className = remove + ' ' + add; - return animateAfter('setClass', element, className, animationCompleted, options.to); - }, + function beforeStart() { + element.addClass(NG_ANIMATE_CLASSNAME); + if (tempClasses) { + $$jqLite.addClass(element, tempClasses); + } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } + } - addClass: function(element, className, animationCompleted, options) { - options = options || {}; - return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to); - }, + function updateAnimationRunners(animation, newRunner) { + if (animation.from && animation.to) { + update(animation.from.element); + update(animation.to.element); + } else { + update(animation.element); + } - removeClass: function(element, className, animationCompleted, options) { - options = options || {}; - return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to); + function update(element) { + getRunner(element).setHost(newRunner); } - }; + } - function suffixClasses(classes, suffix) { - var className = ''; - classes = isArray(classes) ? classes : classes.split(/\s+/); - forEach(classes, function(klass, i) { - if (klass && klass.length > 0) { - className += (i > 0 ? ' ' : '') + klass + suffix; - } - }); - return className; + function handleDestroyedElement() { + var runner = getRunner(element); + if (runner && (event !== 'leave' || !options.$$domOperationFired)) { + runner.end(); + } + } + + function close(rejected) { // jshint ignore:line + element.off('$destroy', handleDestroyedElement); + removeRunner(element); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + + if (tempClasses) { + $$jqLite.removeClass(element, tempClasses); + } + + element.removeClass(NG_ANIMATE_CLASSNAME); + runner.complete(!rejected); + } + }; + }]; +}]; + +/* global angularAnimateModule: true, + + $$AnimateAsyncRunFactory, + $$rAFSchedulerFactory, + $$AnimateChildrenDirective, + $$AnimateQueueProvider, + $$AnimationProvider, + $AnimateCssProvider, + $$AnimateCssDriverProvider, + $$AnimateJsProvider, + $$AnimateJsDriverProvider, +*/ + +/** + * @ngdoc module + * @name ngAnimate + * @description + * + * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app. + * + * <div doc-module-components="ngAnimate"></div> + * + * # Usage + * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based + * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For + * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within + * the HTML element that the animation will be triggered on. + * + * ## Directive Support + * The following directives are "animation aware": + * + * | Directive | Supported Animations | + * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * + * (More information can be found by visiting each the documentation associated with each directive.) + * + * ## CSS-based Animations + * + * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML + * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation. + * + * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: + * + * ```html + * <div ng-if="bool" class="fade"> + * Fade me in out + * </div> + * <button ng-click="bool=true">Fade In!</button> + * <button ng-click="bool=false">Fade Out!</button> + * ``` + * + * Notice the CSS class **fade**? We can now create the CSS transition code that references this class: + * + * ```css + * /* The starting CSS styles for the enter animation */ + * .fade.ng-enter { + * transition:0.5s linear all; + * opacity:0; + * } + * + * /* The finishing CSS styles for the enter animation */ + * .fade.ng-enter.ng-enter-active { + * opacity:1; + * } + * ``` + * + * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two + * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition + * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards. + * + * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions: + * + * ```css + * /* now the element will fade out before it is removed from the DOM */ + * .fade.ng-leave { + * transition:0.5s linear all; + * opacity:1; + * } + * .fade.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class: + * + * ```css + * /* there is no need to define anything inside of the destination + * CSS class since the keyframe will take charge of the animation */ + * .fade.ng-leave { + * animation: my_fade_animation 0.5s linear; + * -webkit-animation: my_fade_animation 0.5s linear; + * } + * + * @keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * + * @-webkit-keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * ``` + * + * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element. + * + * ### CSS Class-based Animations + * + * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different + * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added + * and removed. + * + * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class: + * + * ```html + * <div ng-show="bool" class="fade"> + * Show and hide me + * </div> + * <button ng-click="bool=true">Toggle</button> + * + * <style> + * .fade.ng-hide { + * transition:0.5s linear all; + * opacity:0; + * } + * </style> + * ``` + * + * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since + * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest. + * + * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation + * with CSS styles. + * + * ```html + * <div ng-class="{on:onOff}" class="highlight"> + * Highlight this box + * </div> + * <button ng-click="onOff=!onOff">Toggle</button> + * + * <style> + * .highlight { + * transition:0.5s linear all; + * } + * .highlight.on-add { + * background:white; + * } + * .highlight.on { + * background:yellow; + * } + * .highlight.on-remove { + * background:black; + * } + * </style> + * ``` + * + * We can also make use of CSS keyframes by placing them within the CSS classes. + * + * + * ### CSS Staggering Animations + * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a + * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be + * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for + * the animation. The style property expected within the stagger class can either be a **transition-delay** or an + * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). + * + * ```css + * .my-animation.ng-enter { + * /* standard transition code */ + * transition: 1s linear all; + * opacity:0; + * } + * .my-animation.ng-enter-stagger { + * /* this will have a 100ms delay between each successive leave animation */ + * transition-delay: 0.1s; + * + * /* As of 1.4.4, this must always be set: it signals ngAnimate + * to not accidentally inherit a delay property from another CSS class */ + * transition-duration: 0s; + * } + * .my-animation.ng-enter.ng-enter-active { + * /* standard transition styles */ + * opacity:1; + * } + * ``` + * + * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations + * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this + * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation + * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired. + * + * The following code will issue the **ng-leave-stagger** event on the element provided: + * + * ```js + * var kids = parent.children(); + * + * $animate.leave(kids[0]); //stagger index=0 + * $animate.leave(kids[1]); //stagger index=1 + * $animate.leave(kids[2]); //stagger index=2 + * $animate.leave(kids[3]); //stagger index=3 + * $animate.leave(kids[4]); //stagger index=4 + * + * window.requestAnimationFrame(function() { + * //stagger has reset itself + * $animate.leave(kids[5]); //stagger index=0 + * $animate.leave(kids[6]); //stagger index=1 + * + * $scope.$digest(); + * }); + * ``` + * + * Stagger animations are currently only supported within CSS-defined animations. + * + * ### The `ng-animate` CSS class + * + * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation. + * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations). + * + * Therefore, animations can be applied to an element using this temporary class directly via CSS. + * + * ```css + * .zipper.ng-animate { + * transition:0.5s linear all; + * } + * .zipper.ng-enter { + * opacity:0; + * } + * .zipper.ng-enter.ng-enter-active { + * opacity:1; + * } + * .zipper.ng-leave { + * opacity:1; + * } + * .zipper.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove + * the CSS class once an animation has completed.) + * + * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + * <div ng-class="{red: myProp}"> + * <div ng-class="{blue: myProp}"> + * <div class="message" ng-if="myProp"></div> + * </div> + * </div> + * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * + * ## JavaScript-based Animations + * + * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared + * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the + * `module.animation()` module function we can register the ainmation. + * + * Let's see an example of a enter/leave animation using `ngRepeat`: + * + * ```html + * <div ng-repeat="item in items" class="slide"> + * {{ item }} + * </div> + * ``` + * + * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`: + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * // make note that other events (like addClass/removeClass) + * // have different function input parameters + * enter: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * + * // remember to call doneFn so that angular + * // knows that the animation has concluded + * }, + * + * move: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * }, + * + * leave: function(element, doneFn) { + * jQuery(element).fadeOut(1000, doneFn); + * } + * } + * }]); + * ``` + * + * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as + * greensock.js and velocity.js. + * + * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define + * our animations inside of the same registered animation, however, the function input arguments are a bit different: + * + * ```html + * <div ng-class="color" class="colorful"> + * this box is moody + * </div> + * <button ng-click="color='red'">Change to red</button> + * <button ng-click="color='blue'">Change to blue</button> + * <button ng-click="color='green'">Change to green</button> + * ``` + * + * ```js + * myModule.animation('.colorful', [function() { + * return { + * addClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * removeClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * setClass: function(element, addedClass, removedClass, doneFn) { + * // do some cool animation and call the doneFn + * } + * } + * }]); + * ``` + * + * ## CSS + JS Animations Together + * + * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular, + * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking + * charge of the animation**: + * + * ```html + * <div ng-if="bool" class="slide"> + * Slide in and out + * </div> + * ``` + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * enter: function(element, doneFn) { + * jQuery(element).slideIn(1000, doneFn); + * } + * } + * }]); + * ``` + * + * ```css + * .slide.ng-enter { + * transition:0.5s linear all; + * transform:translateY(-100px); + * } + * .slide.ng-enter.ng-enter-active { + * transform:translateY(0); + * } + * ``` + * + * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the + * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from + * our own JS-based animation code: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { +* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. + * return $animateCss(element, { + * event: 'enter', + * structural: true + * }); + * } + * } + * }]); + * ``` + * + * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. + * + * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or + * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that + * data into `$animateCss` directly: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { + * return $animateCss(element, { + * event: 'enter', + * structural: true, + * addClass: 'maroon-setting', + * from: { height:0 }, + * to: { height: 200 } + * }); + * } + * } + * }]); + * ``` + * + * Now we can fill in the rest via our transition CSS code: + * + * ```css + * /* the transition tells ngAnimate to make the animation happen */ + * .slide.ng-enter { transition:0.5s linear all; } + * + * /* this extra CSS class will be absorbed into the transition + * since the $animateCss code is adding the class */ + * .maroon-setting { background:red; } + * ``` + * + * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over. + * + * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}. + * + * ## Animation Anchoring (via `ng-animate-ref`) + * + * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between + * structural areas of an application (like views) by pairing up elements using an attribute + * called `ng-animate-ref`. + * + * Let's say for example we have two views that are managed by `ng-view` and we want to show + * that there is a relationship between two components situated in within these views. By using the + * `ng-animate-ref` attribute we can identify that the two components are paired together and we + * can then attach an animation, which is triggered when the view changes. + * + * Say for example we have the following template code: + * + * ```html + * <!-- index.html --> + * <div ng-view class="view-animation"> + * </div> + * + * <!-- home.html --> + * <a href="#/banner-page"> + * <img src="./banner.jpg" class="banner" ng-animate-ref="banner"> + * </a> + * + * <!-- banner-page.html --> + * <img src="./banner.jpg" class="banner" ng-animate-ref="banner"> + * ``` + * + * Now, when the view changes (once the link is clicked), ngAnimate will examine the + * HTML contents to see if there is a match reference between any components in the view + * that is leaving and the view that is entering. It will scan both the view which is being + * removed (leave) and inserted (enter) to see if there are any paired DOM elements that + * contain a matching ref value. + * + * The two images match since they share the same ref value. ngAnimate will now create a + * transport element (which is a clone of the first image element) and it will then attempt + * to animate to the position of the second image element in the next view. For the animation to + * work a special CSS class called `ng-anchor` will be added to the transported element. + * + * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then + * ngAnimate will handle the entire transition for us as well as the addition and removal of + * any changes of CSS classes between the elements: + * + * ```css + * .banner.ng-anchor { + * /* this animation will last for 1 second since there are + * two phases to the animation (an `in` and an `out` phase) */ + * transition:0.5s linear all; + * } + * ``` + * + * We also **must** include animations for the views that are being entered and removed + * (otherwise anchoring wouldn't be possible since the new view would be inserted right away). + * + * ```css + * .view-animation.ng-enter, .view-animation.ng-leave { + * transition:0.5s linear all; + * position:fixed; + * left:0; + * top:0; + * width:100%; + * } + * .view-animation.ng-enter { + * transform:translateX(100%); + * } + * .view-animation.ng-leave, + * .view-animation.ng-enter.ng-enter-active { + * transform:translateX(0%); + * } + * .view-animation.ng-leave.ng-leave-active { + * transform:translateX(-100%); + * } + * ``` + * + * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur: + * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away + * from its origin. Once that animation is over then the `in` stage occurs which animates the + * element to its destination. The reason why there are two animations is to give enough time + * for the enter animation on the new element to be ready. + * + * The example above sets up a transition for both the in and out phases, but we can also target the out or + * in phases directly via `ng-anchor-out` and `ng-anchor-in`. + * + * ```css + * .banner.ng-anchor-out { + * transition: 0.5s linear all; + * + * /* the scale will be applied during the out animation, + * but will be animated away when the in animation runs */ + * transform: scale(1.2); + * } + * + * .banner.ng-anchor-in { + * transition: 1s linear all; + * } + * ``` + * + * + * + * + * ### Anchoring Demo + * + <example module="anchoringExample" + name="anchoringExample" + id="anchoringExample" + deps="angular-animate.js;angular-route.js" + animations="true"> + <file name="index.html"> + <a href="#/">Home</a> + <hr /> + <div class="view-container"> + <div ng-view class="view"></div> + </div> + </file> + <file name="script.js"> + angular.module('anchoringExample', ['ngAnimate', 'ngRoute']) + .config(['$routeProvider', function($routeProvider) { + $routeProvider.when('/', { + templateUrl: 'home.html', + controller: 'HomeController as home' + }); + $routeProvider.when('/profile/:id', { + templateUrl: 'profile.html', + controller: 'ProfileController as profile' + }); + }]) + .run(['$rootScope', function($rootScope) { + $rootScope.records = [ + { id:1, title: "Miss Beulah Roob" }, + { id:2, title: "Trent Morissette" }, + { id:3, title: "Miss Ava Pouros" }, + { id:4, title: "Rod Pouros" }, + { id:5, title: "Abdul Rice" }, + { id:6, title: "Laurie Rutherford Sr." }, + { id:7, title: "Nakia McLaughlin" }, + { id:8, title: "Jordon Blanda DVM" }, + { id:9, title: "Rhoda Hand" }, + { id:10, title: "Alexandrea Sauer" } + ]; + }]) + .controller('HomeController', [function() { + //empty + }]) + .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) { + var index = parseInt($routeParams.id, 10); + var record = $rootScope.records[index - 1]; + + this.title = record.title; + this.id = record.id; + }]); + </file> + <file name="home.html"> + <h2>Welcome to the home page</h1> + <p>Please click on an element</p> + <a class="record" + ng-href="#/profile/{{ record.id }}" + ng-animate-ref="{{ record.id }}" + ng-repeat="record in records"> + {{ record.title }} + </a> + </file> + <file name="profile.html"> + <div class="profile record" ng-animate-ref="{{ profile.id }}"> + {{ profile.title }} + </div> + </file> + <file name="animations.css"> + .record { + display:block; + font-size:20px; + } + .profile { + background:black; + color:white; + font-size:100px; } - }]); - }]); + .view-container { + position:relative; + } + .view-container > .view.ng-animate { + position:absolute; + top:0; + left:0; + width:100%; + min-height:500px; + } + .view.ng-enter, .view.ng-leave, + .record.ng-anchor { + transition:0.5s linear all; + } + .view.ng-enter { + transform:translateX(100%); + } + .view.ng-enter.ng-enter-active, .view.ng-leave { + transform:translateX(0%); + } + .view.ng-leave.ng-leave-active { + transform:translateX(-100%); + } + .record.ng-anchor-out { + background:red; + } + </file> + </example> + * + * ### How is the element transported? + * + * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting + * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element + * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The + * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match + * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied + * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class + * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element + * will become visible since the shim class will be removed. + * + * ### How is the morphing handled? + * + * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out + * what CSS classes differ between the starting element and the destination element. These different CSS classes + * will be added/removed on the anchor element and a transition will be applied (the transition that is provided + * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will + * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that + * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since + * the cloned element is placed inside of root element which is likely close to the body element). + * + * Note that if the root element is on the `<html>` element then the cloned node will be placed inside of body. + * + * + * ## Using $animate in your directive code + * + * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application? + * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's + * imagine we have a greeting box that shows and hides itself when the data changes + * + * ```html + * <greeting-box active="onOrOff">Hi there</greeting-box> + * ``` + * + * ```js + * ngModule.directive('greetingBox', ['$animate', function($animate) { + * return function(scope, element, attrs) { + * attrs.$observe('active', function(value) { + * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on'); + * }); + * }); + * }]); + * ``` + * + * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element + * in our HTML code then we can trigger a CSS or JS animation to happen. + * + * ```css + * /* normally we would create a CSS class to reference on the element */ + * greeting-box.on { transition:0.5s linear all; background:green; color:white; } + * ``` + * + * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's + * possible be sure to visit the {@link ng.$animate $animate service API page}. + * + * + * ### Preventing Collisions With Third Party Libraries + * + * Some third-party frameworks place animation duration defaults across many element or className + * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which + * is expecting actual animations on these elements and has to wait for their completion. + * + * You can prevent this unwanted behavior by using a prefix on all your animation classes: + * + * ```css + * /* prefixed with animate- */ + * .animate-fade-add.animate-fade-add-active { + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * You then configure `$animate` to enforce this prefix: + * + * ```js + * $animateProvider.classNameFilter(/animate-/); + * ``` + * + * This also may provide your application with a speed boost since only specific elements containing CSS class prefix + * will be evaluated for animation when any DOM changes occur in the application. + * + * ## Callbacks and Promises + * + * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger + * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has + * ended by chaining onto the returned promise that animation method returns. + * + * ```js + * // somewhere within the depths of the directive + * $animate.enter(element, parent).then(function() { + * //the animation has completed + * }); + * ``` + * + * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case + * anymore.) + * + * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering + * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view + * routing controller to hook into that: + * + * ```js + * ngModule.controller('HomePageController', ['$animate', function($animate) { + * $animate.on('enter', ngViewElement, function(element) { + * // the animation for this route has completed + * }]); + * }]) + * ``` + * + * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.) + */ + +/** + * @ngdoc service + * @name $animate + * @kind object + * + * @description + * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. + * + * Click here {@link ng.$animate to learn more about animations with `$animate`}. + */ +angular.module('ngAnimate', []) + .directive('ngAnimateChildren', $$AnimateChildrenDirective) + .factory('$$rAFScheduler', $$rAFSchedulerFactory) + + .provider('$$animateQueue', $$AnimateQueueProvider) + .provider('$$animation', $$AnimationProvider) + + .provider('$animateCss', $AnimateCssProvider) + .provider('$$animateCssDriver', $$AnimateCssDriverProvider) + + .provider('$$animateJs', $$AnimateJsProvider) + .provider('$$animateJsDriver', $$AnimateJsDriverProvider); })(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-aria.js b/xstatic/pkg/angular/data/angular-aria.js index 19bcbb6..a68b5c9 100644 --- a/xstatic/pkg/angular/data/angular-aria.js +++ b/xstatic/pkg/angular/data/angular-aria.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -19,8 +19,8 @@ * * ## Usage * - * For ngAria to do its magic, simply include the module as a dependency. The directives supported - * by ngAria are: + * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following + * directives are supported: * `ngModel`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, `ngDblClick`, and `ngMessages`. * * Below is a more detailed breakdown of the attributes handled by ngAria: @@ -58,6 +58,16 @@ var ngAriaModule = angular.module('ngAria', ['ng']). provider('$aria', $AriaProvider); /** +* Internal Utilities +*/ +var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY']; + +var isNodeOneOf = function(elem, nodeTypeArray) { + if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) { + return true; + } +}; +/** * @ngdoc provider * @name $ariaProvider * @@ -88,7 +98,8 @@ function $AriaProvider() { ariaMultiline: true, ariaValue: true, tabindex: true, - bindKeypress: true + bindKeypress: true, + bindRoleForClick: true }; /** @@ -107,6 +118,8 @@ function $AriaProvider() { * - **tabindex** – `{boolean}` – Enables/disables tabindex tags * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on `<div>` and * `<li>` elements with ng-click + * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements like `div` + * using ng-click, making them more accessible to users of assistive technologies * * @description * Enables/disables various ARIA attributes @@ -115,20 +128,18 @@ function $AriaProvider() { config = angular.extend(config, newConfig); }; - function watchExpr(attrName, ariaAttr, negate) { + function watchExpr(attrName, ariaAttr, nodeBlackList, negate) { return function(scope, elem, attr) { var ariaCamelName = attr.$normalize(ariaAttr); - if (config[ariaCamelName] && !attr[ariaCamelName]) { + if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) { scope.$watch(attr[attrName], function(boolVal) { - if (negate) { - boolVal = !boolVal; - } + // ensure boolean value + boolVal = negate ? !boolVal : !!boolVal; elem.attr(ariaAttr, boolVal); }); } }; } - /** * @ngdoc service * @name $aria @@ -187,10 +198,10 @@ function $AriaProvider() { ngAriaModule.directive('ngShow', ['$aria', function($aria) { - return $aria.$$watchExpr('ngShow', 'aria-hidden', true); + return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true); }]) .directive('ngHide', ['$aria', function($aria) { - return $aria.$$watchExpr('ngHide', 'aria-hidden', false); + return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false); }]) .directive('ngModel', ['$aria', function($aria) { @@ -229,7 +240,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { } }, post: function(scope, elem, attr, ngModel) { - var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem); + var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem) + && !isNodeOneOf(elem, nodeBlackList); function ngAriaWatchModelValue() { return ngModel.$modelValue; @@ -264,24 +276,40 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? getRadioReaction() : ngAriaCheckboxReaction); } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } break; case 'range': if (shouldAttachRole(shape, elem)) { elem.attr('role', 'slider'); } if ($aria.config('ariaValue')) { - if (attr.min && !elem.attr('aria-valuemin')) { - elem.attr('aria-valuemin', attr.min); + var needsAriaValuemin = !elem.attr('aria-valuemin') && + (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin')); + var needsAriaValuemax = !elem.attr('aria-valuemax') && + (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax')); + var needsAriaValuenow = !elem.attr('aria-valuenow'); + + if (needsAriaValuemin) { + attr.$observe('min', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemin', newVal); + }); } - if (attr.max && !elem.attr('aria-valuemax')) { - elem.attr('aria-valuemax', attr.max); + if (needsAriaValuemax) { + attr.$observe('max', function ngAriaValueMinReaction(newVal) { + elem.attr('aria-valuemax', newVal); + }); } - if (!elem.attr('aria-valuenow')) { + if (needsAriaValuenow) { scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { elem.attr('aria-valuenow', newVal); }); } } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } break; case 'multiline': if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) { @@ -290,10 +318,6 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { break; } - if (needsTabIndex) { - elem.attr('tabindex', 0); - } - if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) { scope.$watch(function ngAriaRequiredWatch() { return ngModel.$error.required; @@ -315,7 +339,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { }; }]) .directive('ngDisabled', ['$aria', function($aria) { - return $aria.$$watchExpr('ngDisabled', 'aria-disabled'); + return $aria.$$watchExpr('ngDisabled', 'aria-disabled', []); }]) .directive('ngMessages', function() { return { @@ -335,31 +359,28 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true); return function(scope, elem, attr) { - var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA']; + if (!isNodeOneOf(elem, nodeBlackList)) { - function isNodeOneOf(elem, nodeTypeArray) { - if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) { - return true; + if ($aria.config('bindRoleForClick') && !elem.attr('role')) { + elem.attr('role', 'button'); } - } - if (!elem.attr('role') && !isNodeOneOf(elem, nodeBlackList)) { - elem.attr('role', 'button'); - } - if ($aria.config('tabindex') && !elem.attr('tabindex')) { - elem.attr('tabindex', 0); - } + if ($aria.config('tabindex') && !elem.attr('tabindex')) { + elem.attr('tabindex', 0); + } - if ($aria.config('bindKeypress') && !attr.ngKeypress && !isNodeOneOf(elem, nodeBlackList)) { - elem.on('keypress', function(event) { - if (event.keyCode === 32 || event.keyCode === 13) { - scope.$apply(callback); - } + if ($aria.config('bindKeypress') && !attr.ngKeypress) { + elem.on('keypress', function(event) { + var keyCode = event.which || event.keyCode; + if (keyCode === 32 || keyCode === 13) { + scope.$apply(callback); + } - function callback() { - fn(scope, { $event: event }); - } - }); + function callback() { + fn(scope, { $event: event }); + } + }); + } } }; } @@ -367,7 +388,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { }]) .directive('ngDblclick', ['$aria', function($aria) { return function(scope, elem, attr) { - if ($aria.config('tabindex') && !elem.attr('tabindex')) { + if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) { elem.attr('tabindex', 0); } }; diff --git a/xstatic/pkg/angular/data/angular-cookies.js b/xstatic/pkg/angular/data/angular-cookies.js index 495d3ba..beb8b62 100644 --- a/xstatic/pkg/angular/data/angular-cookies.js +++ b/xstatic/pkg/angular/data/angular-cookies.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -17,191 +17,306 @@ * * <div doc-module-components="ngCookies"></div> * - * See {@link ngCookies.$cookies `$cookies`} and - * {@link ngCookies.$cookieStore `$cookieStore`} for usage. + * See {@link ngCookies.$cookies `$cookies`} for usage. */ angular.module('ngCookies', ['ng']). /** - * @ngdoc service - * @name $cookies - * + * @ngdoc provider + * @name $cookiesProvider * @description - * Provides read/write access to browser's cookies. - * - * Only a simple Object is exposed and by adding or removing properties to/from this object, new - * cookies are created/deleted at the end of current $eval. - * The object's properties can only be strings. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookiesExample', ['ngCookies']) - * .controller('ExampleController', ['$cookies', function($cookies) { - * // Retrieving a cookie - * var favoriteCookie = $cookies.myFavorite; - * // Setting a cookie - * $cookies.myFavorite = 'oatmeal'; - * }]); - * ``` - */ - factory('$cookies', ['$rootScope', '$browser', function($rootScope, $browser) { - var cookies = {}, - lastCookies = {}, - lastBrowserCookies, - runEval = false, - copy = angular.copy, - isUndefined = angular.isUndefined; - - //creates a poller fn that copies all cookies from the $browser to service & inits the service - $browser.addPollFn(function() { - var currentCookies = $browser.cookies(); - if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl - lastBrowserCookies = currentCookies; - copy(currentCookies, lastCookies); - copy(currentCookies, cookies); - if (runEval) $rootScope.$apply(); - } - })(); - - runEval = true; - - //at the end of each eval, push cookies - //TODO: this should happen before the "delayed" watches fire, because if some cookies are not - // strings or browser refuses to store some cookies, we update the model in the push fn. - $rootScope.$watch(push); - - return cookies; - - - /** - * Pushes all the cookies from the service to the browser and verifies if all cookies were - * stored. - */ - function push() { - var name, - value, - browserCookies, - updated; - - //delete any cookies deleted in $cookies - for (name in lastCookies) { - if (isUndefined(cookies[name])) { - $browser.cookies(name, undefined); - delete lastCookies[name]; - } - } - - //update all cookies updated in $cookies - for (name in cookies) { - value = cookies[name]; - if (!angular.isString(value)) { - value = '' + value; - cookies[name] = value; - } - if (value !== lastCookies[name]) { - $browser.cookies(name, value); - lastCookies[name] = value; - updated = true; - } - } - - //verify what was actually stored - if (updated) { - browserCookies = $browser.cookies(); - - for (name in cookies) { - if (cookies[name] !== browserCookies[name]) { - //delete or reset all cookies that the browser dropped from $cookies - if (isUndefined(browserCookies[name])) { - delete cookies[name]; - delete lastCookies[name]; - } else { - cookies[name] = lastCookies[name] = browserCookies[name]; - } - } - } - } - } - }]). - + * Use `$cookiesProvider` to change the default behavior of the {@link ngCookies.$cookies $cookies} service. + * */ + provider('$cookies', [function $CookiesProvider() { + /** + * @ngdoc property + * @name $cookiesProvider#defaults + * @description + * + * Object containing default options to pass when setting cookies. + * + * The object may have following properties: + * + * - **path** - `{string}` - The cookie will be available only for this path and its + * sub-paths. By default, this is the URL that appears in your `<base>` tag. + * - **domain** - `{string}` - The cookie will be available only for this domain and + * its sub-domains. For security reasons the user agent will not accept the cookie + * if the current domain is not a sub-domain of this domain or equal to it. + * - **expires** - `{string|Date}` - String of the form "Wdy, DD Mon YYYY HH:MM:SS GMT" + * or a Date object indicating the exact date/time this cookie will expire. + * - **secure** - `{boolean}` - If `true`, then the cookie will only be available through a + * secured connection. + * + * Note: By default, the address that appears in your `<base>` tag will be used as the path. + * This is important so that cookies will be visible for all routes when html5mode is enabled. + * + **/ + var defaults = this.defaults = {}; - /** - * @ngdoc service - * @name $cookieStore - * @requires $cookies - * - * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. - * - * Requires the {@link ngCookies `ngCookies`} module to be installed. - * - * @example - * - * ```js - * angular.module('cookieStoreExample', ['ngCookies']) - * .controller('ExampleController', ['$cookieStore', function($cookieStore) { - * // Put cookie - * $cookieStore.put('myFavorite','oatmeal'); - * // Get cookie - * var favoriteCookie = $cookieStore.get('myFavorite'); - * // Removing a cookie - * $cookieStore.remove('myFavorite'); - * }]); - * ``` - */ - factory('$cookieStore', ['$cookies', function($cookies) { + function calcOptions(options) { + return options ? angular.extend({}, defaults, options) : defaults; + } + /** + * @ngdoc service + * @name $cookies + * + * @description + * Provides read/write access to browser's cookies. + * + * <div class="alert alert-info"> + * Up until Angular 1.3, `$cookies` exposed properties that represented the + * current browser cookie values. In version 1.4, this behavior has changed, and + * `$cookies` now provides a standard api of getters, setters etc. + * </div> + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * @example + * + * ```js + * angular.module('cookiesExample', ['ngCookies']) + * .controller('ExampleController', ['$cookies', function($cookies) { + * // Retrieving a cookie + * var favoriteCookie = $cookies.get('myFavorite'); + * // Setting a cookie + * $cookies.put('myFavorite', 'oatmeal'); + * }]); + * ``` + */ + this.$get = ['$$cookieReader', '$$cookieWriter', function($$cookieReader, $$cookieWriter) { return { /** * @ngdoc method - * @name $cookieStore#get + * @name $cookies#get * * @description * Returns the value of given cookie key * * @param {string} key Id to use for lookup. - * @returns {Object} Deserialized cookie value. + * @returns {string} Raw cookie value. */ get: function(key) { - var value = $cookies[key]; + return $$cookieReader()[key]; + }, + + /** + * @ngdoc method + * @name $cookies#getObject + * + * @description + * Returns the deserialized value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value. + */ + getObject: function(key) { + var value = this.get(key); return value ? angular.fromJson(value) : value; }, /** * @ngdoc method - * @name $cookieStore#put + * @name $cookies#getAll + * + * @description + * Returns a key value object with all the cookies + * + * @returns {Object} All cookies + */ + getAll: function() { + return $$cookieReader(); + }, + + /** + * @ngdoc method + * @name $cookies#put * * @description * Sets a value for given cookie key * * @param {string} key Id for the `value`. + * @param {string} value Raw value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} + */ + put: function(key, value, options) { + $$cookieWriter(key, value, calcOptions(options)); + }, + + /** + * @ngdoc method + * @name $cookies#putObject + * + * @description + * Serializes and sets a value for given cookie key + * + * @param {string} key Id for the `value`. * @param {Object} value Value to be stored. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} */ - put: function(key, value) { - $cookies[key] = angular.toJson(value); + putObject: function(key, value, options) { + this.put(key, angular.toJson(value), options); }, /** * @ngdoc method - * @name $cookieStore#remove + * @name $cookies#remove * * @description * Remove given cookie * * @param {string} key Id of the key-value pair to delete. + * @param {Object=} options Options object. + * See {@link ngCookies.$cookiesProvider#defaults $cookiesProvider.defaults} */ - remove: function(key) { - delete $cookies[key]; + remove: function(key, options) { + $$cookieWriter(key, undefined, calcOptions(options)); } }; + }]; + }]); + +angular.module('ngCookies'). +/** + * @ngdoc service + * @name $cookieStore + * @deprecated + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * + * Requires the {@link ngCookies `ngCookies`} module to be installed. + * + * <div class="alert alert-danger"> + * **Note:** The $cookieStore service is **deprecated**. + * Please use the {@link ngCookies.$cookies `$cookies`} service instead. + * </div> + * + * @example + * + * ```js + * angular.module('cookieStoreExample', ['ngCookies']) + * .controller('ExampleController', ['$cookieStore', function($cookieStore) { + * // Put cookie + * $cookieStore.put('myFavorite','oatmeal'); + * // Get cookie + * var favoriteCookie = $cookieStore.get('myFavorite'); + * // Removing a cookie + * $cookieStore.remove('myFavorite'); + * }]); + * ``` + */ + factory('$cookieStore', ['$cookies', function($cookies) { + + return { + /** + * @ngdoc method + * @name $cookieStore#get + * + * @description + * Returns the value of given cookie key + * + * @param {string} key Id to use for lookup. + * @returns {Object} Deserialized cookie value, undefined if the cookie does not exist. + */ + get: function(key) { + return $cookies.getObject(key); + }, + + /** + * @ngdoc method + * @name $cookieStore#put + * + * @description + * Sets a value for given cookie key + * + * @param {string} key Id for the `value`. + * @param {Object} value Value to be stored. + */ + put: function(key, value) { + $cookies.putObject(key, value); + }, + + /** + * @ngdoc method + * @name $cookieStore#remove + * + * @description + * Remove given cookie + * + * @param {string} key Id of the key-value pair to delete. + */ + remove: function(key) { + $cookies.remove(key); + } + }; + + }]); + +/** + * @name $$cookieWriter + * @requires $document + * + * @description + * This is a private service for writing cookies + * + * @param {string} name Cookie name + * @param {string=} value Cookie value (if undefined, cookie will be deleted) + * @param {Object=} options Object with options that need to be stored for the cookie. + */ +function $$CookieWriter($document, $log, $browser) { + var cookiePath = $browser.baseHref(); + var rawDocument = $document[0]; + + function buildCookieString(name, value, options) { + var path, expires; + options = options || {}; + expires = options.expires; + path = angular.isDefined(options.path) ? options.path : cookiePath; + if (angular.isUndefined(value)) { + expires = 'Thu, 01 Jan 1970 00:00:00 GMT'; + value = ''; + } + if (angular.isString(expires)) { + expires = new Date(expires); + } + + var str = encodeURIComponent(name) + '=' + encodeURIComponent(value); + str += path ? ';path=' + path : ''; + str += options.domain ? ';domain=' + options.domain : ''; + str += expires ? ';expires=' + expires.toUTCString() : ''; + str += options.secure ? ';secure' : ''; + + // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: + // - 300 cookies + // - 20 cookies per unique domain + // - 4096 bytes per cookie + var cookieLength = str.length + 1; + if (cookieLength > 4096) { + $log.warn("Cookie '" + name + + "' possibly not set or overflowed because it was too large (" + + cookieLength + " > 4096 bytes)!"); + } + + return str; + } + + return function(name, value, options) { + rawDocument.cookie = buildCookieString(name, value, options); + }; +} + +$$CookieWriter.$inject = ['$document', '$log', '$browser']; - }]); +angular.module('ngCookies').provider('$$cookieWriter', function $$CookieWriterProvider() { + this.$get = $$CookieWriter; +}); })(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-csp.css b/xstatic/pkg/angular/data/angular-csp.css index 0ce9d86..f3cd926 100644 --- a/xstatic/pkg/angular/data/angular-csp.css +++ b/xstatic/pkg/angular/data/angular-csp.css @@ -11,3 +11,11 @@ ng\:form { display: block; } + +.ng-animate-shim { + visibility:hidden; +} + +.ng-anchor { + position:absolute; +} diff --git a/xstatic/pkg/angular/data/angular-loader.js b/xstatic/pkg/angular/data/angular-loader.js index fd9b2a1..f90a5c7 100644 --- a/xstatic/pkg/angular/data/angular-loader.js +++ b/xstatic/pkg/angular/data/angular-loader.js @@ -1,10 +1,39 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function() {'use strict'; + function isFunction(value) {return typeof value === 'function';}; + +/* global: toDebugString: true */ + +function serializeObject(obj) { + var seen = []; + + return JSON.stringify(obj, function(key, val) { + val = toJsonReplacer(key, val); + if (isObject(val)) { + + if (seen.indexOf(val) >= 0) return '...'; + + seen.push(val); + } + return val; + }); +} + +function toDebugString(obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (isUndefined(obj)) { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj); + } + return obj; +} /** * @description @@ -39,28 +68,33 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; return function() { - var code = arguments[0], - prefix = '[' + (module ? module + ':' : '') + code + '] ', - template = arguments[1], - templateArgs = arguments, + var SKIP_INDEXES = 2; - message, i; + var templateArgs = arguments, + code = templateArgs[0], + message = '[' + (module ? module + ':' : '') + code + '] ', + template = templateArgs[1], + paramPrefix, i; - message = prefix + template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), arg; + message += template.replace(/\{\d+\}/g, function(match) { + var index = +match.slice(1, -1), + shiftedIndex = index + SKIP_INDEXES; - if (index + 2 < templateArgs.length) { - return toDebugString(templateArgs[index + 2]); + if (shiftedIndex < templateArgs.length) { + return toDebugString(templateArgs[shiftedIndex]); } + return match; }); - message = message + '\nhttp://errors.angularjs.org/1.3.18/' + + message += '\nhttp://errors.angularjs.org/1.4.10/' + (module ? module + '/' : '') + code; - for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + - encodeURIComponent(toDebugString(arguments[i])); + + for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + + encodeURIComponent(toDebugString(templateArgs[i])); } + return new ErrorConstructor(message); }; } @@ -103,8 +137,8 @@ function setupModuleLoader(window) { * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. + * Passing one argument retrieves an existing {@link angular.Module}, + * whereas passing more than one argument creates a new {@link angular.Module} * * * # Module @@ -141,7 +175,7 @@ function setupModuleLoader(window) { * unspecified then the module is being retrieved for further configuration. * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. + * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { var assertNotHasOwnProperty = function(name, context) { @@ -211,7 +245,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#provider $provide.provider()}. */ - provider: invokeLater('$provide', 'provider'), + provider: invokeLaterAndSetModuleName('$provide', 'provider'), /** * @ngdoc method @@ -222,7 +256,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#factory $provide.factory()}. */ - factory: invokeLater('$provide', 'factory'), + factory: invokeLaterAndSetModuleName('$provide', 'factory'), /** * @ngdoc method @@ -233,7 +267,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#service $provide.service()}. */ - service: invokeLater('$provide', 'service'), + service: invokeLaterAndSetModuleName('$provide', 'service'), /** * @ngdoc method @@ -253,11 +287,23 @@ function setupModuleLoader(window) { * @param {string} name constant name * @param {*} object Constant value. * @description - * Because the constant are fixed, they get applied before other provide methods. + * Because the constants are fixed, they get applied before other provide methods. * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#decorator + * @module ng + * @param {string} The name of the service to decorate. + * @param {Function} This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. + * @description + * See {@link auto.$provide#decorator $provide.decorator()}. + */ + decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + /** * @ngdoc method * @name angular.Module#animation @@ -271,7 +317,7 @@ function setupModuleLoader(window) { * * * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. + * {@link $animate $animate} service and directives that use this service. * * ```js * module.animation('.animation-name', function($inject1, $inject2) { @@ -290,7 +336,7 @@ function setupModuleLoader(window) { * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ - animation: invokeLater('$animateProvider', 'register'), + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), /** * @ngdoc method @@ -308,7 +354,7 @@ function setupModuleLoader(window) { * (`myapp_subsection_filterx`). * </div> */ - filter: invokeLater('$filterProvider', 'register'), + filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), /** * @ngdoc method @@ -320,7 +366,7 @@ function setupModuleLoader(window) { * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. */ - controller: invokeLater('$controllerProvider', 'register'), + controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), /** * @ngdoc method @@ -333,7 +379,7 @@ function setupModuleLoader(window) { * @description * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ - directive: invokeLater('$compileProvider', 'directive'), + directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), /** * @ngdoc method @@ -383,6 +429,19 @@ function setupModuleLoader(window) { return moduleInstance; }; } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method) { + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + }; + } }); }; }); diff --git a/xstatic/pkg/angular/data/angular-message-format.js b/xstatic/pkg/angular/data/angular-message-format.js new file mode 100644 index 0000000..2ec4e31 --- /dev/null +++ b/xstatic/pkg/angular/data/angular-message-format.js @@ -0,0 +1,980 @@ +/** + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +var $interpolateMinErr = window['angular']['$interpolateMinErr']; + +var noop = window['angular']['noop'], + isFunction = window['angular']['isFunction'], + toJson = window['angular']['toJson']; + +function stringify(value) { + if (value == null /* null/undefined */) { return ''; } + switch (typeof value) { + case 'string': return value; + case 'number': return '' + value; + default: return toJson(value); + } +} + +// Convert an index into the string into line/column for use in error messages +// As such, this doesn't have to be efficient. +function indexToLineAndColumn(text, index) { + var lines = text.split(/\n/g); + for (var i=0; i < lines.length; i++) { + var line=lines[i]; + if (index >= line.length) { + index -= line.length; + } else { + return { line: i + 1, column: index + 1 }; + } + } +} +var PARSE_CACHE_FOR_TEXT_LITERALS = Object.create(null); + +function parseTextLiteral(text) { + var cachedFn = PARSE_CACHE_FOR_TEXT_LITERALS[text]; + if (cachedFn != null) { + return cachedFn; + } + function parsedFn(context) { return text; } + parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { + var unwatch = scope['$watch'](noop, + function textLiteralWatcher() { + if (isFunction(listener)) { listener.call(null, text, text, scope); } + unwatch(); + }, + objectEquality); + return unwatch; + }; + PARSE_CACHE_FOR_TEXT_LITERALS[text] = parsedFn; + parsedFn['exp'] = text; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + parsedFn['expressions'] = []; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + return parsedFn; +} + +function subtractOffset(expressionFn, offset) { + if (offset === 0) { + return expressionFn; + } + function minusOffset(value) { + return (value == void 0) ? value : value - offset; + } + function parsedFn(context) { return minusOffset(expressionFn(context)); } + var unwatch; + parsedFn['$$watchDelegate'] = function watchDelegate(scope, listener, objectEquality) { + unwatch = scope['$watch'](expressionFn, + function pluralExpressionWatchListener(newValue, oldValue) { + if (isFunction(listener)) { listener.call(null, minusOffset(newValue), minusOffset(oldValue), scope); } + }, + objectEquality); + return unwatch; + }; + return parsedFn; +} + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global isFunction: false */ +/* global noop: false */ + +/** + * @constructor + * @private + */ +function MessageSelectorBase(expressionFn, choices) { + var self = this; + this.expressionFn = expressionFn; + this.choices = choices; + if (choices["other"] === void 0) { + throw $interpolateMinErr('reqother', '“other” is a required option.'); + } + this.parsedFn = function(context) { return self.getResult(context); }; + this.parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { + return self.watchDelegate(scope, listener, objectEquality); + }; + this.parsedFn['exp'] = expressionFn['exp']; + this.parsedFn['expressions'] = expressionFn['expressions']; +} + +MessageSelectorBase.prototype.getMessageFn = function getMessageFn(value) { + return this.choices[this.categorizeValue(value)]; +}; + +MessageSelectorBase.prototype.getResult = function getResult(context) { + return this.getMessageFn(this.expressionFn(context))(context); +}; + +MessageSelectorBase.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { + var watchers = new MessageSelectorWatchers(this, scope, listener, objectEquality); + return function() { watchers.cancelWatch(); }; +}; + +/** + * @constructor + * @private + */ +function MessageSelectorWatchers(msgSelector, scope, listener, objectEquality) { + var self = this; + this.scope = scope; + this.msgSelector = msgSelector; + this.listener = listener; + this.objectEquality = objectEquality; + this.lastMessage = void 0; + this.messageFnWatcher = noop; + var expressionFnListener = function(newValue, oldValue) { return self.expressionFnListener(newValue, oldValue); }; + this.expressionFnWatcher = scope['$watch'](msgSelector.expressionFn, expressionFnListener, objectEquality); +} + +MessageSelectorWatchers.prototype.expressionFnListener = function expressionFnListener(newValue, oldValue) { + var self = this; + this.messageFnWatcher(); + var messageFnListener = function(newMessage, oldMessage) { return self.messageFnListener(newMessage, oldMessage); }; + var messageFn = this.msgSelector.getMessageFn(newValue); + this.messageFnWatcher = this.scope['$watch'](messageFn, messageFnListener, this.objectEquality); +}; + +MessageSelectorWatchers.prototype.messageFnListener = function messageFnListener(newMessage, oldMessage) { + if (isFunction(this.listener)) { + this.listener.call(null, newMessage, newMessage === oldMessage ? newMessage : this.lastMessage, this.scope); + } + this.lastMessage = newMessage; +}; + +MessageSelectorWatchers.prototype.cancelWatch = function cancelWatch() { + this.expressionFnWatcher(); + this.messageFnWatcher(); +}; + +/** + * @constructor + * @extends MessageSelectorBase + * @private + */ +function SelectMessage(expressionFn, choices) { + MessageSelectorBase.call(this, expressionFn, choices); +} + +function SelectMessageProto() {} +SelectMessageProto.prototype = MessageSelectorBase.prototype; + +SelectMessage.prototype = new SelectMessageProto(); +SelectMessage.prototype.categorizeValue = function categorizeSelectValue(value) { + return (this.choices[value] !== void 0) ? value : "other"; +}; + +/** + * @constructor + * @extends MessageSelectorBase + * @private + */ +function PluralMessage(expressionFn, choices, offset, pluralCat) { + MessageSelectorBase.call(this, expressionFn, choices); + this.offset = offset; + this.pluralCat = pluralCat; +} + +function PluralMessageProto() {} +PluralMessageProto.prototype = MessageSelectorBase.prototype; + +PluralMessage.prototype = new PluralMessageProto(); +PluralMessage.prototype.categorizeValue = function categorizePluralValue(value) { + if (isNaN(value)) { + return "other"; + } else if (this.choices[value] !== void 0) { + return value; + } else { + var category = this.pluralCat(value - this.offset); + return (this.choices[category] !== void 0) ? category : "other"; + } +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global isFunction: false */ +/* global parseTextLiteral: false */ + +/** + * @constructor + * @private + */ +function InterpolationParts(trustedContext, allOrNothing) { + this.trustedContext = trustedContext; + this.allOrNothing = allOrNothing; + this.textParts = []; + this.expressionFns = []; + this.expressionIndices = []; + this.partialText = ''; + this.concatParts = null; +} + +InterpolationParts.prototype.flushPartialText = function flushPartialText() { + if (this.partialText) { + if (this.concatParts == null) { + this.textParts.push(this.partialText); + } else { + this.textParts.push(this.concatParts.join('')); + this.concatParts = null; + } + this.partialText = ''; + } +}; + +InterpolationParts.prototype.addText = function addText(text) { + if (text.length) { + if (!this.partialText) { + this.partialText = text; + } else if (this.concatParts) { + this.concatParts.push(text); + } else { + this.concatParts = [this.partialText, text]; + } + } +}; + +InterpolationParts.prototype.addExpressionFn = function addExpressionFn(expressionFn) { + this.flushPartialText(); + this.expressionIndices.push(this.textParts.length); + this.expressionFns.push(expressionFn); + this.textParts.push(''); +}; + +InterpolationParts.prototype.getExpressionValues = function getExpressionValues(context) { + var expressionValues = new Array(this.expressionFns.length); + for (var i = 0; i < this.expressionFns.length; i++) { + expressionValues[i] = this.expressionFns[i](context); + } + return expressionValues; +}; + +InterpolationParts.prototype.getResult = function getResult(expressionValues) { + for (var i = 0; i < this.expressionIndices.length; i++) { + var expressionValue = expressionValues[i]; + if (this.allOrNothing && expressionValue === void 0) return; + this.textParts[this.expressionIndices[i]] = expressionValue; + } + return this.textParts.join(''); +}; + + +InterpolationParts.prototype.toParsedFn = function toParsedFn(mustHaveExpression, originalText) { + var self = this; + this.flushPartialText(); + if (mustHaveExpression && this.expressionFns.length === 0) { + return void 0; + } + if (this.textParts.length === 0) { + return parseTextLiteral(''); + } + if (this.trustedContext && this.textParts.length > 1) { + $interpolateMinErr['throwNoconcat'](originalText); + } + if (this.expressionFns.length === 0) { + if (this.textParts.length != 1) { this.errorInParseLogic(); } + return parseTextLiteral(this.textParts[0]); + } + var parsedFn = function(context) { + return self.getResult(self.getExpressionValues(context)); + }; + parsedFn['$$watchDelegate'] = function $$watchDelegate(scope, listener, objectEquality) { + return self.watchDelegate(scope, listener, objectEquality); + }; + + parsedFn['exp'] = originalText; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + parsedFn['expressions'] = new Array(this.expressionFns.length); // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + for (var i = 0; i < this.expressionFns.length; i++) { + parsedFn['expressions'][i] = this.expressionFns[i]['exp']; + } + + return parsedFn; +}; + +InterpolationParts.prototype.watchDelegate = function watchDelegate(scope, listener, objectEquality) { + var watcher = new InterpolationPartsWatcher(this, scope, listener, objectEquality); + return function() { watcher.cancelWatch(); }; +}; + +function InterpolationPartsWatcher(interpolationParts, scope, listener, objectEquality) { + this.interpolationParts = interpolationParts; + this.scope = scope; + this.previousResult = (void 0); + this.listener = listener; + var self = this; + this.expressionFnsWatcher = scope['$watchGroup'](interpolationParts.expressionFns, function(newExpressionValues, oldExpressionValues) { + self.watchListener(newExpressionValues, oldExpressionValues); + }); +} + +InterpolationPartsWatcher.prototype.watchListener = function watchListener(newExpressionValues, oldExpressionValues) { + var result = this.interpolationParts.getResult(newExpressionValues); + if (isFunction(this.listener)) { + this.listener.call(null, result, newExpressionValues === oldExpressionValues ? result : this.previousResult, this.scope); + } + this.previousResult = result; +}; + +InterpolationPartsWatcher.prototype.cancelWatch = function cancelWatch() { + this.expressionFnsWatcher(); +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global indexToLineAndColumn: false */ +/* global InterpolationParts: false */ +/* global PluralMessage: false */ +/* global SelectMessage: false */ +/* global subtractOffset: false */ + +// The params src and dst are exactly one of two types: NestedParserState or MessageFormatParser. +// This function is fully optimized by V8. (inspect via IRHydra or --trace-deopt.) +// The idea behind writing it this way is to avoid repeating oneself. This is the ONE place where +// the parser state that is saved/restored when parsing nested mustaches is specified. +function copyNestedParserState(src, dst) { + dst.expressionFn = src.expressionFn; + dst.expressionMinusOffsetFn = src.expressionMinusOffsetFn; + dst.pluralOffset = src.pluralOffset; + dst.choices = src.choices; + dst.choiceKey = src.choiceKey; + dst.interpolationParts = src.interpolationParts; + dst.ruleChoiceKeyword = src.ruleChoiceKeyword; + dst.msgStartIndex = src.msgStartIndex; + dst.expressionStartIndex = src.expressionStartIndex; +} + +function NestedParserState(parser) { + copyNestedParserState(parser, this); +} + +/** + * @constructor + * @private + */ +function MessageFormatParser(text, startIndex, $parse, pluralCat, stringifier, + mustHaveExpression, trustedContext, allOrNothing) { + this.text = text; + this.index = startIndex || 0; + this.$parse = $parse; + this.pluralCat = pluralCat; + this.stringifier = stringifier; + this.mustHaveExpression = !!mustHaveExpression; + this.trustedContext = trustedContext; + this.allOrNothing = !!allOrNothing; + this.expressionFn = null; + this.expressionMinusOffsetFn = null; + this.pluralOffset = null; + this.choices = null; + this.choiceKey = null; + this.interpolationParts = null; + this.msgStartIndex = null; + this.nestedStateStack = []; + this.parsedFn = null; + this.rule = null; + this.ruleStack = null; + this.ruleChoiceKeyword = null; + this.interpNestLevel = null; + this.expressionStartIndex = null; + this.stringStartIndex = null; + this.stringQuote = null; + this.stringInterestsRe = null; + this.angularOperatorStack = null; + this.textPart = null; +} + +// preserve v8 optimization. +var EMPTY_STATE = new NestedParserState(new MessageFormatParser( + /* text= */ '', /* startIndex= */ 0, /* $parse= */ null, /* pluralCat= */ null, /* stringifier= */ null, + /* mustHaveExpression= */ false, /* trustedContext= */ null, /* allOrNothing */ false)); + +MessageFormatParser.prototype.pushState = function pushState() { + this.nestedStateStack.push(new NestedParserState(this)); + copyNestedParserState(EMPTY_STATE, this); +}; + +MessageFormatParser.prototype.popState = function popState() { + if (this.nestedStateStack.length === 0) { + this.errorInParseLogic(); + } + var previousState = this.nestedStateStack.pop(); + copyNestedParserState(previousState, this); +}; + +// Oh my JavaScript! Who knew you couldn't match a regex at a specific +// location in a string but will always search forward?! +// Apparently you'll be growing this ability via the sticky flag (y) in +// ES6. I'll just to work around you for now. +MessageFormatParser.prototype.matchRe = function matchRe(re, search) { + re.lastIndex = this.index; + var match = re.exec(this.text); + if (match != null && (search === true || (match.index == this.index))) { + this.index = re.lastIndex; + return match; + } + return null; +}; + +MessageFormatParser.prototype.searchRe = function searchRe(re) { + return this.matchRe(re, true); +}; + + +MessageFormatParser.prototype.consumeRe = function consumeRe(re) { + // Without the sticky flag, we can't use the .test() method to consume a + // match at the current index. Instead, we'll use the slower .exec() method + // and verify match.index. + return !!this.matchRe(re); +}; + +// Run through our grammar avoiding deeply nested function call chains. +MessageFormatParser.prototype.run = function run(initialRule) { + this.ruleStack = [initialRule]; + do { + this.rule = this.ruleStack.pop(); + while (this.rule) { + this.rule(); + } + this.assertRuleOrNull(this.rule); + } while (this.ruleStack.length > 0); +}; + +MessageFormatParser.prototype.errorInParseLogic = function errorInParseLogic() { + throw $interpolateMinErr('logicbug', + 'The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”', + this.text); +}; + +MessageFormatParser.prototype.assertRuleOrNull = function assertRuleOrNull(rule) { + if (rule === void 0) { + this.errorInParseLogic(); + } +}; + +var NEXT_WORD_RE = /\s*(\w+)\s*/g; +MessageFormatParser.prototype.errorExpecting = function errorExpecting() { + // What was wrong with the syntax? Unsupported type, missing comma, or something else? + var match = this.matchRe(NEXT_WORD_RE), position; + if (match == null) { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqarg', + 'Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”', + position.line, position.column, this.text); + } + var word = match[1]; + if (word == "select" || word == "plural") { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqcomma', + 'Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”', + word, position.line, position.column, this.text); + } else { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('unknarg', + 'Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”', + word, position.line, position.column, this.text); + } +}; + +var STRING_START_RE = /['"]/g; +MessageFormatParser.prototype.ruleString = function ruleString() { + var match = this.matchRe(STRING_START_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('wantstring', + 'Expected the beginning of a string at line {0}, column {1} in text “{2}”', + position.line, position.column, this.text); + } + this.startStringAtMatch(match); +}; + +MessageFormatParser.prototype.startStringAtMatch = function startStringAtMatch(match) { + this.stringStartIndex = match.index; + this.stringQuote = match[0]; + this.stringInterestsRe = this.stringQuote == "'" ? SQUOTED_STRING_INTEREST_RE : DQUOTED_STRING_INTEREST_RE; + this.rule = this.ruleInsideString; +}; + +var SQUOTED_STRING_INTEREST_RE = /\\(?:\\|'|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|'/g; +var DQUOTED_STRING_INTEREST_RE = /\\(?:\\|"|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{2}|[0-7]{3}|\r\n|\n|[\s\S])|"/g; +MessageFormatParser.prototype.ruleInsideString = function ruleInsideString() { + var match = this.searchRe(this.stringInterestsRe); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.stringStartIndex); + throw $interpolateMinErr('untermstr', + 'The string beginning at line {0}, column {1} is unterminated in text “{2}”', + position.line, position.column, this.text); + } + var chars = match[0]; + if (match == this.stringQuote) { + this.rule = null; + } +}; + +var PLURAL_OR_SELECT_ARG_TYPE_RE = /\s*(plural|select)\s*,\s*/g; +MessageFormatParser.prototype.rulePluralOrSelect = function rulePluralOrSelect() { + var match = this.searchRe(PLURAL_OR_SELECT_ARG_TYPE_RE); + if (match == null) { + this.errorExpecting(); + } + var argType = match[1]; + switch (argType) { + case "plural": this.rule = this.rulePluralStyle; break; + case "select": this.rule = this.ruleSelectStyle; break; + default: this.errorInParseLogic(); + } +}; + +MessageFormatParser.prototype.rulePluralStyle = function rulePluralStyle() { + this.choices = Object.create(null); + this.ruleChoiceKeyword = this.rulePluralValueOrKeyword; + this.rule = this.rulePluralOffset; +}; + +MessageFormatParser.prototype.ruleSelectStyle = function ruleSelectStyle() { + this.choices = Object.create(null); + this.ruleChoiceKeyword = this.ruleSelectKeyword; + this.rule = this.ruleSelectKeyword; +}; + +var NUMBER_RE = /[0]|(?:[1-9][0-9]*)/g; +var PLURAL_OFFSET_RE = new RegExp("\\s*offset\\s*:\\s*(" + NUMBER_RE.source + ")", "g"); + +MessageFormatParser.prototype.rulePluralOffset = function rulePluralOffset() { + var match = this.matchRe(PLURAL_OFFSET_RE); + this.pluralOffset = (match == null) ? 0 : parseInt(match[1], 10); + this.expressionMinusOffsetFn = subtractOffset(this.expressionFn, this.pluralOffset); + this.rule = this.rulePluralValueOrKeyword; +}; + +MessageFormatParser.prototype.assertChoiceKeyIsNew = function assertChoiceKeyIsNew(choiceKey, index) { + if (this.choices[choiceKey] !== void 0) { + var position = indexToLineAndColumn(this.text, index); + throw $interpolateMinErr('dupvalue', + 'The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”', + choiceKey, position.line, position.column, this.text); + } +}; + +var SELECT_KEYWORD = /\s*(\w+)/g; +MessageFormatParser.prototype.ruleSelectKeyword = function ruleSelectKeyword() { + var match = this.matchRe(SELECT_KEYWORD); + if (match == null) { + this.parsedFn = new SelectMessage(this.expressionFn, this.choices).parsedFn; + this.rule = null; + return; + } + this.choiceKey = match[1]; + this.assertChoiceKeyIsNew(this.choiceKey, match.index); + this.rule = this.ruleMessageText; +}; + +var EXPLICIT_VALUE_OR_KEYWORD_RE = new RegExp("\\s*(?:(?:=(" + NUMBER_RE.source + "))|(\\w+))", "g"); +MessageFormatParser.prototype.rulePluralValueOrKeyword = function rulePluralValueOrKeyword() { + var match = this.matchRe(EXPLICIT_VALUE_OR_KEYWORD_RE); + if (match == null) { + this.parsedFn = new PluralMessage(this.expressionFn, this.choices, this.pluralOffset, this.pluralCat).parsedFn; + this.rule = null; + return; + } + if (match[1] != null) { + this.choiceKey = parseInt(match[1], 10); + } else { + this.choiceKey = match[2]; + } + this.assertChoiceKeyIsNew(this.choiceKey, match.index); + this.rule = this.ruleMessageText; +}; + +var BRACE_OPEN_RE = /\s*{/g; +var BRACE_CLOSE_RE = /}/g; +MessageFormatParser.prototype.ruleMessageText = function ruleMessageText() { + if (!this.consumeRe(BRACE_OPEN_RE)) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqopenbrace', + 'The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”', + this.choiceKey, position.line, position.column, this.text); + } + this.msgStartIndex = this.index; + this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); + this.rule = this.ruleInInterpolationOrMessageText; +}; + +// Note: Since "\" is used as an escape character, don't allow it to be part of the +// startSymbol/endSymbol when I add the feature to allow them to be redefined. +var INTERP_OR_END_MESSAGE_RE = /\\.|{{|}/g; +var INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE = /\\.|{{|#|}/g; +var ESCAPE_OR_MUSTACHE_BEGIN_RE = /\\.|{{/g; +MessageFormatParser.prototype.advanceInInterpolationOrMessageText = function advanceInInterpolationOrMessageText() { + var currentIndex = this.index, match, re; + if (this.ruleChoiceKeyword == null) { // interpolation + match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); + if (match == null) { // End of interpolation text. Nothing more to process. + this.textPart = this.text.substring(currentIndex); + this.index = this.text.length; + return null; + } + } else { + match = this.searchRe(this.ruleChoiceKeyword == this.rulePluralValueOrKeyword ? + INTERP_OR_PLURALVALUE_OR_END_MESSAGE_RE : INTERP_OR_END_MESSAGE_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.msgStartIndex); + throw $interpolateMinErr('reqendbrace', + 'The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”', + this.choiceKey, position.line, position.column, this.text); + } + } + // match is non-null. + var token = match[0]; + this.textPart = this.text.substring(currentIndex, match.index); + return token; +}; + +MessageFormatParser.prototype.ruleInInterpolationOrMessageText = function ruleInInterpolationOrMessageText() { + var currentIndex = this.index; + var token = this.advanceInInterpolationOrMessageText(); + if (token == null) { + // End of interpolation text. Nothing more to process. + this.index = this.text.length; + this.interpolationParts.addText(this.text.substring(currentIndex)); + this.rule = null; + return; + } + if (token[0] == "\\") { + // unescape next character and continue + this.interpolationParts.addText(this.textPart + token[1]); + return; + } + this.interpolationParts.addText(this.textPart); + if (token == "{{") { + this.pushState(); + this.ruleStack.push(this.ruleEndMustacheInInterpolationOrMessage); + this.rule = this.ruleEnteredMustache; + } else if (token == "}") { + this.choices[this.choiceKey] = this.interpolationParts.toParsedFn(/*mustHaveExpression=*/false, this.text); + this.rule = this.ruleChoiceKeyword; + } else if (token == "#") { + this.interpolationParts.addExpressionFn(this.expressionMinusOffsetFn); + } else { + this.errorInParseLogic(); + } +}; + +MessageFormatParser.prototype.ruleInterpolate = function ruleInterpolate() { + this.interpolationParts = new InterpolationParts(this.trustedContext, this.allOrNothing); + this.rule = this.ruleInInterpolation; +}; + +MessageFormatParser.prototype.ruleInInterpolation = function ruleInInterpolation() { + var currentIndex = this.index; + var match = this.searchRe(ESCAPE_OR_MUSTACHE_BEGIN_RE); + if (match == null) { + // End of interpolation text. Nothing more to process. + this.index = this.text.length; + this.interpolationParts.addText(this.text.substring(currentIndex)); + this.parsedFn = this.interpolationParts.toParsedFn(this.mustHaveExpression, this.text); + this.rule = null; + return; + } + var token = match[0]; + if (token[0] == "\\") { + // unescape next character and continue + this.interpolationParts.addText(this.text.substring(currentIndex, match.index) + token[1]); + return; + } + this.interpolationParts.addText(this.text.substring(currentIndex, match.index)); + this.pushState(); + this.ruleStack.push(this.ruleInterpolationEndMustache); + this.rule = this.ruleEnteredMustache; +}; + +MessageFormatParser.prototype.ruleInterpolationEndMustache = function ruleInterpolationEndMustache() { + var expressionFn = this.parsedFn; + this.popState(); + this.interpolationParts.addExpressionFn(expressionFn); + this.rule = this.ruleInInterpolation; +}; + +MessageFormatParser.prototype.ruleEnteredMustache = function ruleEnteredMustache() { + this.parsedFn = null; + this.ruleStack.push(this.ruleEndMustache); + this.rule = this.ruleAngularExpression; +}; + +MessageFormatParser.prototype.ruleEndMustacheInInterpolationOrMessage = function ruleEndMustacheInInterpolationOrMessage() { + var expressionFn = this.parsedFn; + this.popState(); + this.interpolationParts.addExpressionFn(expressionFn); + this.rule = this.ruleInInterpolationOrMessageText; +}; + + + +var INTERP_END_RE = /\s*}}/g; +MessageFormatParser.prototype.ruleEndMustache = function ruleEndMustache() { + var match = this.matchRe(INTERP_END_RE); + if (match == null) { + var position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('reqendinterp', + 'Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”', + '}}', position.line, position.column, this.text); + } + if (this.parsedFn == null) { + // If we parsed a MessageFormat extension, (e.g. select/plural today, maybe more some other + // day), then the result *has* to be a string and those rules would have already set + // this.parsedFn. If there was no MessageFormat extension, then there is no requirement to + // stringify the result and parsedFn isn't set. We set it here. While we could have set it + // unconditionally when exiting the Angular expression, I intend for us to not just replace + // $interpolate, but also to replace $parse in a future version (so ng-bind can work), and in + // such a case we do not want to unnecessarily stringify something if it's not going to be used + // in a string context. + this.parsedFn = this.$parse(this.expressionFn, this.stringifier); + this.parsedFn['exp'] = this.expressionFn['exp']; // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.parsedFn['expressions'] = this.expressionFn['expressions']; // Require this to call $compile.$$addBindingInfo() which allows Protractor to find elements by binding. + } + this.rule = null; +}; + +MessageFormatParser.prototype.ruleAngularExpression = function ruleAngularExpression() { + this.angularOperatorStack = []; + this.expressionStartIndex = this.index; + this.rule = this.ruleInAngularExpression; +}; + +function getEndOperator(opBegin) { + switch (opBegin) { + case "{": return "}"; + case "[": return "]"; + case "(": return ")"; + default: return null; + } +} + +function getBeginOperator(opEnd) { + switch (opEnd) { + case "}": return "{"; + case "]": return "["; + case ")": return "("; + default: return null; + } +} + +// TODO(chirayu): The interpolation endSymbol must also be accounted for. It +// just so happens that "}" is an operator so it's in the list below. But we +// should support any other type of start/end interpolation symbol. +var INTERESTING_OPERATORS_RE = /[[\]{}()'",]/g; +MessageFormatParser.prototype.ruleInAngularExpression = function ruleInAngularExpression() { + var startIndex = this.index; + var match = this.searchRe(INTERESTING_OPERATORS_RE); + var position; + if (match == null) { + if (this.angularOperatorStack.length === 0) { + // This is the end of the Angular expression so this is actually a + // success. Note that when inside an interpolation, this means we even + // consumed the closing interpolation symbols if they were curlies. This + // is NOT an error at this point but will become an error further up the + // stack when the part that saw the opening curlies is unable to find the + // closing ones. + this.index = this.text.length; + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; + return; + } + var innermostOperator = this.angularOperatorStack[0]; + throw $interpolateMinErr('badexpr', + 'Unexpected end of Angular expression. Expecting operator “{0}” at the end of the text “{1}”', + this.getEndOperator(innermostOperator), this.text); + } + var operator = match[0]; + if (operator == "'" || operator == '"') { + this.ruleStack.push(this.ruleInAngularExpression); + this.startStringAtMatch(match); + return; + } + if (operator == ",") { + if (this.trustedContext) { + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('unsafe', + 'Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”', + this.trustedContext, position.line, position.column, this.text); + } + // only the top level comma has relevance. + if (this.angularOperatorStack.length === 0) { + // todo: does this need to be trimmed? + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, match.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, match.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; + this.rule = this.rulePluralOrSelect; + } + return; + } + if (getEndOperator(operator) != null) { + this.angularOperatorStack.unshift(operator); + return; + } + var beginOperator = getBeginOperator(operator); + if (beginOperator == null) { + this.errorInParseLogic(); + } + if (this.angularOperatorStack.length > 0) { + if (beginOperator == this.angularOperatorStack[0]) { + this.angularOperatorStack.shift(); + return; + } + position = indexToLineAndColumn(this.text, this.index); + throw $interpolateMinErr('badexpr', + 'Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”', + operator, position.line, position.column, getEndOperator(this.angularOperatorStack[0]), this.text); + } + // We are trying to pop off the operator stack but there really isn't anything to pop off. + this.index = match.index; + this.expressionFn = this.$parse(this.text.substring(this.expressionStartIndex, this.index)); + // Needed to pretend to be $interpolate for tests copied from interpolateSpec.js + this.expressionFn['exp'] = this.text.substring(this.expressionStartIndex, this.index); + this.expressionFn['expressions'] = this.expressionFn['expressions']; + this.rule = null; +}; + +// NOTE: ADVANCED_OPTIMIZATIONS mode. +// +// This file is compiled with Closure compiler's ADVANCED_OPTIMIZATIONS flag! Be wary of using +// constructs incompatible with that mode. + +/* global $interpolateMinErr: false */ +/* global MessageFormatParser: false */ +/* global stringify: false */ + +/** + * @ngdoc service + * @name $$messageFormat + * + * @description + * Angular internal service to recognize MessageFormat extensions in interpolation expressions. + * For more information, see: + * https://docs.google.com/a/google.com/document/d/1pbtW2yvtmFBikfRrJd8VAsabiFkKezmYZ_PbgdjQOVU/edit + * + * ## Example + * + * <example name="ngMessageFormat-example" module="msgFmtExample" deps="angular-message-format.min.js"> + * <file name="index.html"> + * <div ng-controller="AppController"> + * <button ng-click="decreaseRecipients()" id="decreaseRecipients">decreaseRecipients</button><br> + * <span>{{recipients.length, plural, offset:1 + * =0 {{{sender.name}} gave no gifts (\#=#)} + * =1 {{{sender.name}} gave one gift to {{recipients[0].name}} (\#=#)} + * one {{{sender.name}} gave {{recipients[0].name}} and one other person a gift (\#=#)} + * other {{{sender.name}} gave {{recipients[0].name}} and # other people a gift (\#=#)} + * }}</span> + * </div> + * </file> + * + * <file name="script.js"> + * function Person(name, gender) { + * this.name = name; + * this.gender = gender; + * } + * + * var alice = new Person("Alice", "female"), + * bob = new Person("Bob", "male"), + * charlie = new Person("Charlie", "male"), + * harry = new Person("Harry Potter", "male"); + * + * angular.module('msgFmtExample', ['ngMessageFormat']) + * .controller('AppController', ['$scope', function($scope) { + * $scope.recipients = [alice, bob, charlie]; + * $scope.sender = harry; + * $scope.decreaseRecipients = function() { + * --$scope.recipients.length; + * }; + * }]); + * </file> + * + * <file name="protractor.js" type="protractor"> + * describe('MessageFormat plural', function() { + * it('should pluralize initial values', function() { + * var messageElem = element(by.binding('recipients.length')), decreaseRecipientsBtn = element(by.id('decreaseRecipients')); + * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and 2 other people a gift (#=2)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave Alice and one other person a gift (#=1)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave one gift to Alice (#=0)'); + * decreaseRecipientsBtn.click(); + * expect(messageElem.getText()).toEqual('Harry Potter gave no gifts (#=-1)'); + * }); + * }); + * </file> + * </example> + */ +var $$MessageFormatFactory = ['$parse', '$locale', '$sce', '$exceptionHandler', function $$messageFormat( + $parse, $locale, $sce, $exceptionHandler) { + + function getStringifier(trustedContext, allOrNothing, text) { + return function stringifier(value) { + try { + value = trustedContext ? $sce['getTrusted'](trustedContext, value) : $sce['valueOf'](value); + return allOrNothing && (value === void 0) ? value : stringify(value); + } catch (err) { + $exceptionHandler($interpolateMinErr['interr'](text, err)); + } + }; + } + + function interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + var stringifier = getStringifier(trustedContext, allOrNothing, text); + var parser = new MessageFormatParser(text, 0, $parse, $locale['pluralCat'], stringifier, + mustHaveExpression, trustedContext, allOrNothing); + parser.run(parser.ruleInterpolate); + return parser.parsedFn; + } + + return { + 'interpolate': interpolate + }; +}]; + +var $$interpolateDecorator = ['$$messageFormat', '$delegate', function $$interpolateDecorator($$messageFormat, $interpolate) { + if ($interpolate['startSymbol']() != "{{" || $interpolate['endSymbol']() != "}}") { + throw $interpolateMinErr('nochgmustache', 'angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.'); + } + var interpolate = $$messageFormat['interpolate']; + interpolate['startSymbol'] = $interpolate['startSymbol']; + interpolate['endSymbol'] = $interpolate['endSymbol']; + return interpolate; +}]; + + +/** + * @ngdoc module + * @name ngMessageFormat + * @packageName angular-message-format + * @description + */ +var module = window['angular']['module']('ngMessageFormat', ['ng']); +module['factory']('$$messageFormat', $$MessageFormatFactory); +module['config'](['$provide', function($provide) { + $provide['decorator']('$interpolate', $$interpolateDecorator); +}]); + + +})(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-messages.js b/xstatic/pkg/angular/data/angular-messages.js index 6d32f5c..ad55265 100644 --- a/xstatic/pkg/angular/data/angular-messages.js +++ b/xstatic/pkg/angular/data/angular-messages.js @@ -1,10 +1,18 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; +/* jshint ignore:start */ +// this code is in the core, but not in angular-messages.js +var isArray = angular.isArray; +var forEach = angular.forEach; +var isString = angular.isString; +var jqLite = angular.element; +/* jshint ignore:end */ + /** * @ngdoc module * @name ngMessages @@ -17,47 +25,77 @@ * `ngMessage` directives are designed to handle the complexity, inheritance and priority * sequencing based on the order of how the messages are defined in the template. * - * Currently, the ngMessages module only contains the code for the `ngMessages` - * and `ngMessage` directives. + * Currently, the ngMessages module only contains the code for the `ngMessages`, `ngMessagesInclude` + * `ngMessage` and `ngMessageExp` directives. * * # Usage - * The `ngMessages` directive listens on a key/value collection which is set on the ngMessages attribute. - * Since the {@link ngModel ngModel} directive exposes an `$error` object, this error object can be - * used with `ngMessages` to display control error messages in an easier way than with just regular angular - * template directives. + * The `ngMessages` directive allows keys in a key/value collection to be associated with a child element + * (or 'message') that will show or hide based on the truthiness of that key's value in the collection. A common use + * case for `ngMessages` is to display error messages for inputs using the `$error` object exposed by the + * {@link ngModel ngModel} directive. + * + * The child elements of the `ngMessages` directive are matched to the collection keys by a `ngMessage` or + * `ngMessageExp` directive. The value of these attributes must match a key in the collection that is provided by + * the `ngMessages` directive. + * + * Consider the following example, which illustrates a typical use case of `ngMessages`. Within the form `myForm` we + * have a text input named `myField` which is bound to the scope variable `field` using the {@link ngModel ngModel} + * directive. + * + * The `myField` field is a required input of type `email` with a maximum length of 15 characters. * * ```html * <form name="myForm"> - * <input type="text" ng-model="field" name="myField" required minlength="5" /> - * <div ng-messages="myForm.myField.$error"> - * <div ng-message="required">You did not enter a field</div> - * <div ng-message="minlength">The value entered is too short</div> + * <label> + * Enter text: + * <input type="email" ng-model="field" name="myField" required maxlength="15" /> + * </label> + * <div ng-messages="myForm.myField.$error" role="alert"> + * <div ng-message="required">Please enter a value for this field.</div> + * <div ng-message="email">This field must be a valid email address.</div> + * <div ng-message="maxlength">This field can be at most 15 characters long.</div> * </div> * </form> * ``` * - * Now whatever key/value entries are present within the provided object (in this case `$error`) then - * the ngMessages directive will render the inner first ngMessage directive (depending if the key values - * match the attribute value present on each ngMessage directive). In other words, if your errors - * object contains the following data: + * In order to show error messages corresponding to `myField` we first create an element with an `ngMessages` attribute + * set to the `$error` object owned by the `myField` input in our `myForm` form. + * + * Within this element we then create separate elements for each of the possible errors that `myField` could have. + * The `ngMessage` attribute is used to declare which element(s) will appear for which error - for example, + * setting `ng-message="required"` specifies that this particular element should be displayed when there + * is no value present for the required field `myField` (because the key `required` will be `true` in the object + * `myForm.myField.$error`). + * + * ### Message order + * + * By default, `ngMessages` will only display one message for a particular key/value collection at any time. If more + * than one message (or error) key is currently true, then which message is shown is determined by the order of messages + * in the HTML template code (messages declared first are prioritised). This mechanism means the developer does not have + * to prioritise messages using custom JavaScript code. + * + * Given the following error object for our example (which informs us that the field `myField` currently has both the + * `required` and `email` errors): * * ```javascript * <!-- keep in mind that ngModel automatically sets these error flags --> - * myField.$error = { minlength : true, required : false }; + * myField.$error = { required : true, email: true, maxlength: false }; * ``` + * The `required` message will be displayed to the user since it appears before the `email` message in the DOM. + * Once the user types a single character, the `required` message will disappear (since the field now has a value) + * but the `email` message will be visible because it is still applicable. * - * Then the `required` message will be displayed first. When required is false then the `minlength` message - * will be displayed right after (since these messages are ordered this way in the template HTML code). - * The prioritization of each message is determined by what order they're present in the DOM. - * Therefore, instead of having custom JavaScript code determine the priority of what errors are - * present before others, the presentation of the errors are handled within the template. + * ### Displaying multiple messages at the same time * - * By default, ngMessages will only display one error at a time. However, if you wish to display all - * messages then the `ng-messages-multiple` attribute flag can be used on the element containing the - * ngMessages directive to make this happen. + * While `ngMessages` will by default only display one error element at a time, the `ng-messages-multiple` attribute can + * be applied to the `ngMessages` container element to cause it to display all applicable error messages at once: * * ```html + * <!-- attribute-style usage --> * <div ng-messages="myForm.myField.$error" ng-messages-multiple>...</div> + * + * <!-- element-style usage --> + * <ng-messages for="myForm.myField.$error" multiple>...</ng-messages> * ``` * * ## Reusing and Overriding Messages @@ -70,12 +108,15 @@ * <div ng-message="required">This field is required</div> * <div ng-message="minlength">This field is too short</div> * </script> - * <div ng-messages="myForm.myField.$error" ng-messages-include="error-messages"></div> + * + * <div ng-messages="myForm.myField.$error" role="alert"> + * <div ng-messages-include="error-messages"></div> + * </div> * ``` * * However, including generic messages may not be useful enough to match all input fields, therefore, * `ngMessages` provides the ability to override messages defined in the remote template by redefining - * then within the directive container. + * them within the directive container. * * ```html * <!-- a generic template of error messages known as "my-custom-messages" --> @@ -85,19 +126,26 @@ * </script> * * <form name="myForm"> - * <input type="email" - * id="email" - * name="myEmail" - * ng-model="email" - * minlength="5" - * required /> - * - * <div ng-messages="myForm.myEmail.$error" ng-messages-include="my-custom-messages"> + * <label> + * Email address + * <input type="email" + * id="email" + * name="myEmail" + * ng-model="email" + * minlength="5" + * required /> + * </label> + * <!-- any ng-message elements that appear BEFORE the ng-messages-include will + * override the messages present in the ng-messages-include template --> + * <div ng-messages="myForm.myEmail.$error" role="alert"> * <!-- this required message has overridden the template message --> * <div ng-message="required">You did not enter your email address</div> * * <!-- this is a brand new message and will appear last in the prioritization --> * <div ng-message="email">Your email address is invalid</div> + * + * <!-- and here are the generic error messages --> + * <div ng-messages-include="my-custom-messages"></div> * </div> * </form> * ``` @@ -107,20 +155,80 @@ * email addresses, date fields, autocomplete inputs, etc...), specialized error messages can be applied * while more generic messages can be used to handle other, more general input errors. * + * ## Dynamic Messaging + * ngMessages also supports using expressions to dynamically change key values. Using arrays and + * repeaters to list messages is also supported. This means that the code below will be able to + * fully adapt itself and display the appropriate message when any of the expression data changes: + * + * ```html + * <form name="myForm"> + * <label> + * Email address + * <input type="email" + * name="myEmail" + * ng-model="email" + * minlength="5" + * required /> + * </label> + * <div ng-messages="myForm.myEmail.$error" role="alert"> + * <div ng-message="required">You did not enter your email address</div> + * <div ng-repeat="errorMessage in errorMessages"> + * <!-- use ng-message-exp for a message whose key is given by an expression --> + * <div ng-message-exp="errorMessage.type">{{ errorMessage.text }}</div> + * </div> + * </div> + * </form> + * ``` + * + * The `errorMessage.type` expression can be a string value or it can be an array so + * that multiple errors can be associated with a single error message: + * + * ```html + * <label> + * Email address + * <input type="email" + * ng-model="data.email" + * name="myEmail" + * ng-minlength="5" + * ng-maxlength="100" + * required /> + * </label> + * <div ng-messages="myForm.myEmail.$error" role="alert"> + * <div ng-message-exp="'required'">You did not enter your email address</div> + * <div ng-message-exp="['minlength', 'maxlength']"> + * Your email must be between 5 and 100 characters long + * </div> + * </div> + * ``` + * + * Feel free to use other structural directives such as ng-if and ng-switch to further control + * what messages are active and when. Be careful, if you place ng-message on the same element + * as these structural directives, Angular may not be able to determine if a message is active + * or not. Therefore it is best to place the ng-message on a child element of the structural + * directive. + * + * ```html + * <div ng-messages="myForm.myEmail.$error" role="alert"> + * <div ng-if="showRequiredError"> + * <div ng-message="required">Please enter something</div> + * </div> + * </div> + * ``` + * * ## Animations - * If the `ngAnimate` module is active within the application then both the `ngMessages` and - * `ngMessage` directives will trigger animations whenever any messages are added and removed - * from the DOM by the `ngMessages` directive. + * If the `ngAnimate` module is active within the application then the `ngMessages`, `ngMessage` and + * `ngMessageExp` directives will trigger animations whenever any messages are added and removed from + * the DOM by the `ngMessages` directive. * * Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS * class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no - * animations present. Therefore, CSS transitions and keyframes as well as JavaScript animations can + * messages present. Therefore, CSS transitions and keyframes as well as JavaScript animations can * hook into the animations whenever these classes are added/removed. * * Let's say that our HTML code for our messages container looks like so: * * ```html - * <div ng-messages="myMessages" class="my-messages"> + * <div ng-messages="myMessages" class="my-messages" role="alert"> * <div ng-message="alert" class="some-message">...</div> * <div ng-message="fail" class="some-message">...</div> * </div> @@ -169,14 +277,14 @@ angular.module('ngMessages', []) * * @description * `ngMessages` is a directive that is designed to show and hide messages based on the state - * of a key/value object that it listens on. The directive itself compliments error message + * of a key/value object that it listens on. The directive itself complements error message * reporting with the `ngModel` $error object (which stores a key/value state of validation errors). * * `ngMessages` manages the state of internal messages within its container element. The internal * messages use the `ngMessage` directive and will be inserted/removed from the page depending * on if they're present within the key/value object. By default, only one message will be displayed * at a time and this depends on the prioritization of the messages within the template. (This can - * be changed by using the ng-messages-multiple on the directive container.) + * be changed by using the `ng-messages-multiple` or `multiple` attribute on the directive container.) * * A remote template can also be used to promote message reusability and messages can also be * overridden. @@ -186,24 +294,23 @@ angular.module('ngMessages', []) * @usage * ```html * <!-- using attribute directives --> - * <ANY ng-messages="expression"> - * <ANY ng-message="keyValue1">...</ANY> - * <ANY ng-message="keyValue2">...</ANY> - * <ANY ng-message="keyValue3">...</ANY> + * <ANY ng-messages="expression" role="alert"> + * <ANY ng-message="stringValue">...</ANY> + * <ANY ng-message="stringValue1, stringValue2, ...">...</ANY> + * <ANY ng-message-exp="expressionValue">...</ANY> * </ANY> * * <!-- or by using element directives --> - * <ng-messages for="expression"> - * <ng-message when="keyValue1">...</ng-message> - * <ng-message when="keyValue2">...</ng-message> - * <ng-message when="keyValue3">...</ng-message> + * <ng-messages for="expression" role="alert"> + * <ng-message when="stringValue">...</ng-message> + * <ng-message when="stringValue1, stringValue2, ...">...</ng-message> + * <ng-message when-exp="expressionValue">...</ng-message> * </ng-messages> * ``` * * @param {string} ngMessages an angular expression evaluating to a key/value object * (this is typically the $error object on an ngModel instance). * @param {string=} ngMessagesMultiple|multiple when set, all messages will be displayed with true - * @param {string=} ngMessagesInclude|include when set, the specified template will be included into the ng-messages container * * @example * <example name="ngMessages-directive" module="ngMessagesExample" @@ -211,17 +318,18 @@ angular.module('ngMessages', []) * animations="true" fixBase="true"> * <file name="index.html"> * <form name="myForm"> - * <label>Enter your name:</label> - * <input type="text" - * name="myName" - * ng-model="name" - * ng-minlength="5" - * ng-maxlength="20" - * required /> - * + * <label> + * Enter your name: + * <input type="text" + * name="myName" + * ng-model="name" + * ng-minlength="5" + * ng-maxlength="20" + * required /> + * </label> * <pre>myForm.myName.$error = {{ myForm.myName.$error | json }}</pre> * - * <div ng-messages="myForm.myName.$error" style="color:maroon"> + * <div ng-messages="myForm.myName.$error" style="color:maroon" role="alert"> * <div ng-message="required">You did not enter a field</div> * <div ng-message="minlength">Your field is too short</div> * <div ng-message="maxlength">Your field is too long</div> @@ -233,91 +341,223 @@ angular.module('ngMessages', []) * </file> * </example> */ - .directive('ngMessages', ['$compile', '$animate', '$templateRequest', - function($compile, $animate, $templateRequest) { - var ACTIVE_CLASS = 'ng-active'; - var INACTIVE_CLASS = 'ng-inactive'; + .directive('ngMessages', ['$animate', function($animate) { + var ACTIVE_CLASS = 'ng-active'; + var INACTIVE_CLASS = 'ng-inactive'; - return { - restrict: 'AE', - controller: function() { - this.$renderNgMessageClasses = angular.noop; - - var messages = []; - this.registerMessage = function(index, message) { - for (var i = 0; i < messages.length; i++) { - if (messages[i].type == message.type) { - if (index != i) { - var temp = messages[index]; - messages[index] = messages[i]; - if (index < messages.length) { - messages[i] = temp; - } else { - messages.splice(0, i); //remove the old one (and shift left) - } - } - return; - } - } - messages.splice(index, 0, message); //add the new one (and shift right) - }; + return { + require: 'ngMessages', + restrict: 'AE', + controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + var ctrl = this; + var latestKey = 0; + var nextAttachId = 0; - this.renderMessages = function(values, multiple) { - values = values || {}; + this.getAttachId = function getAttachId() { return nextAttachId++; }; - var found; - angular.forEach(messages, function(message) { - if ((!found || multiple) && truthyVal(values[message.type])) { - message.attach(); - found = true; - } else { - message.detach(); - } - }); + var messages = this.messages = {}; + var renderLater, cachedCollection; - this.renderElementClasses(found); + this.render = function(collection) { + collection = collection || {}; - function truthyVal(value) { - return value !== null && value !== false && value; - } - }; - }, - require: 'ngMessages', - link: function($scope, element, $attrs, ctrl) { - ctrl.renderElementClasses = function(bool) { - bool ? $animate.setClass(element, ACTIVE_CLASS, INACTIVE_CLASS) - : $animate.setClass(element, INACTIVE_CLASS, ACTIVE_CLASS); - }; + renderLater = false; + cachedCollection = collection; - //JavaScript treats empty strings as false, but ng-message-multiple by itself is an empty string - var multiple = angular.isString($attrs.ngMessagesMultiple) || - angular.isString($attrs.multiple); + // this is true if the attribute is empty or if the attribute value is truthy + var multiple = isAttrTruthy($scope, $attrs.ngMessagesMultiple) || + isAttrTruthy($scope, $attrs.multiple); - var cachedValues, watchAttr = $attrs.ngMessages || $attrs['for']; //for is a reserved keyword - $scope.$watchCollection(watchAttr, function(values) { - cachedValues = values; - ctrl.renderMessages(values, multiple); - }); + var unmatchedMessages = []; + var matchedKeys = {}; + var messageItem = ctrl.head; + var messageFound = false; + var totalMessages = 0; - var tpl = $attrs.ngMessagesInclude || $attrs.include; - if (tpl) { - $templateRequest(tpl) - .then(function processTemplate(html) { - var after, container = angular.element('<div/>').html(html); - angular.forEach(container.children(), function(elm) { - elm = angular.element(elm); - after ? after.after(elm) - : element.prepend(elm); //start of the container - after = elm; - $compile(elm)($scope); - }); - ctrl.renderMessages(cachedValues, multiple); - }); - } - } - }; - }]) + // we use != instead of !== to allow for both undefined and null values + while (messageItem != null) { + totalMessages++; + var messageCtrl = messageItem.message; + + var messageUsed = false; + if (!messageFound) { + forEach(collection, function(value, key) { + if (!messageUsed && truthy(value) && messageCtrl.test(key)) { + // this is to prevent the same error name from showing up twice + if (matchedKeys[key]) return; + matchedKeys[key] = true; + + messageUsed = true; + messageCtrl.attach(); + } + }); + } + + if (messageUsed) { + // unless we want to display multiple messages then we should + // set a flag here to avoid displaying the next message in the list + messageFound = !multiple; + } else { + unmatchedMessages.push(messageCtrl); + } + + messageItem = messageItem.next; + } + + forEach(unmatchedMessages, function(messageCtrl) { + messageCtrl.detach(); + }); + + unmatchedMessages.length !== totalMessages + ? $animate.setClass($element, ACTIVE_CLASS, INACTIVE_CLASS) + : $animate.setClass($element, INACTIVE_CLASS, ACTIVE_CLASS); + }; + + $scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render); + + this.reRender = function() { + if (!renderLater) { + renderLater = true; + $scope.$evalAsync(function() { + if (renderLater) { + cachedCollection && ctrl.render(cachedCollection); + } + }); + } + }; + + this.register = function(comment, messageCtrl) { + var nextKey = latestKey.toString(); + messages[nextKey] = { + message: messageCtrl + }; + insertMessageNode($element[0], comment, nextKey); + comment.$$ngMessageNode = nextKey; + latestKey++; + + ctrl.reRender(); + }; + + this.deregister = function(comment) { + var key = comment.$$ngMessageNode; + delete comment.$$ngMessageNode; + removeMessageNode($element[0], comment, key); + delete messages[key]; + ctrl.reRender(); + }; + + function findPreviousMessage(parent, comment) { + var prevNode = comment; + var parentLookup = []; + while (prevNode && prevNode !== parent) { + var prevKey = prevNode.$$ngMessageNode; + if (prevKey && prevKey.length) { + return messages[prevKey]; + } + + // dive deeper into the DOM and examine its children for any ngMessage + // comments that may be in an element that appears deeper in the list + if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) == -1) { + parentLookup.push(prevNode); + prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; + } else { + prevNode = prevNode.previousSibling || prevNode.parentNode; + } + } + } + + function insertMessageNode(parent, comment, key) { + var messageNode = messages[key]; + if (!ctrl.head) { + ctrl.head = messageNode; + } else { + var match = findPreviousMessage(parent, comment); + if (match) { + messageNode.next = match.next; + match.next = messageNode; + } else { + messageNode.next = ctrl.head; + ctrl.head = messageNode; + } + } + } + function removeMessageNode(parent, comment, key) { + var messageNode = messages[key]; + + var match = findPreviousMessage(parent, comment); + if (match) { + match.next = messageNode.next; + } else { + ctrl.head = messageNode.next; + } + } + }] + }; + + function isAttrTruthy(scope, attr) { + return (isString(attr) && attr.length === 0) || //empty attribute + truthy(scope.$eval(attr)); + } + + function truthy(val) { + return isString(val) ? val.length : !!val; + } + }]) + + /** + * @ngdoc directive + * @name ngMessagesInclude + * @restrict AE + * @scope + * + * @description + * `ngMessagesInclude` is a directive with the purpose to import existing ngMessage template + * code from a remote template and place the downloaded template code into the exact spot + * that the ngMessagesInclude directive is placed within the ngMessages container. This allows + * for a series of pre-defined messages to be reused and also allows for the developer to + * determine what messages are overridden due to the placement of the ngMessagesInclude directive. + * + * @usage + * ```html + * <!-- using attribute directives --> + * <ANY ng-messages="expression" role="alert"> + * <ANY ng-messages-include="remoteTplString">...</ANY> + * </ANY> + * + * <!-- or by using element directives --> + * <ng-messages for="expression" role="alert"> + * <ng-messages-include src="expressionValue1">...</ng-messages-include> + * </ng-messages> + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {string} ngMessagesInclude|src a string value corresponding to the remote template. + */ + .directive('ngMessagesInclude', + ['$templateRequest', '$document', '$compile', function($templateRequest, $document, $compile) { + + return { + restrict: 'AE', + require: '^^ngMessages', // we only require this for validation sake + link: function($scope, element, attrs) { + var src = attrs.ngMessagesInclude || attrs.src; + $templateRequest(src).then(function(html) { + $compile(html)($scope, function(contents) { + element.after(contents); + + // the anchor is placed for debugging purposes + var anchor = jqLite($document[0].createComment(' ngMessagesInclude: ' + src + ' ')); + element.after(anchor); + + // we don't want to pollute the DOM anymore by keeping an empty directive element + element.remove(); + }); + }); + } + }; + }]) /** * @ngdoc directive @@ -337,65 +577,130 @@ angular.module('ngMessages', []) * @usage * ```html * <!-- using attribute directives --> + * <ANY ng-messages="expression" role="alert"> + * <ANY ng-message="stringValue">...</ANY> + * <ANY ng-message="stringValue1, stringValue2, ...">...</ANY> + * </ANY> + * + * <!-- or by using element directives --> + * <ng-messages for="expression" role="alert"> + * <ng-message when="stringValue">...</ng-message> + * <ng-message when="stringValue1, stringValue2, ...">...</ng-message> + * </ng-messages> + * ``` + * + * @param {expression} ngMessage|when a string value corresponding to the message key. + */ + .directive('ngMessage', ngMessageDirectiveFactory('AE')) + + + /** + * @ngdoc directive + * @name ngMessageExp + * @restrict AE + * @scope + * + * @description + * `ngMessageExp` is a directive with the purpose to show and hide a particular message. + * For `ngMessageExp` to operate, a parent `ngMessages` directive on a parent DOM element + * must be situated since it determines which messages are visible based on the state + * of the provided key/value map that `ngMessages` listens on. + * + * @usage + * ```html + * <!-- using attribute directives --> * <ANY ng-messages="expression"> - * <ANY ng-message="keyValue1">...</ANY> - * <ANY ng-message="keyValue2">...</ANY> - * <ANY ng-message="keyValue3">...</ANY> + * <ANY ng-message-exp="expressionValue">...</ANY> * </ANY> * * <!-- or by using element directives --> * <ng-messages for="expression"> - * <ng-message when="keyValue1">...</ng-message> - * <ng-message when="keyValue2">...</ng-message> - * <ng-message when="keyValue3">...</ng-message> + * <ng-message when-exp="expressionValue">...</ng-message> * </ng-messages> * ``` * - * @param {string} ngMessage a string value corresponding to the message key. + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {expression} ngMessageExp|whenExp an expression value corresponding to the message key. */ - .directive('ngMessage', ['$animate', function($animate) { - var COMMENT_NODE = 8; + .directive('ngMessageExp', ngMessageDirectiveFactory('A')); + +function ngMessageDirectiveFactory(restrict) { + return ['$animate', function($animate) { return { - require: '^ngMessages', + restrict: 'AE', transclude: 'element', terminal: true, - restrict: 'AE', - link: function($scope, $element, $attrs, ngMessages, $transclude) { - var index, element; - - var commentNode = $element[0]; - var parentNode = commentNode.parentNode; - for (var i = 0, j = 0; i < parentNode.childNodes.length; i++) { - var node = parentNode.childNodes[i]; - if (node.nodeType == COMMENT_NODE && node.nodeValue.indexOf('ngMessage') >= 0) { - if (node === commentNode) { - index = j; - break; - } - j++; - } + require: '^^ngMessages', + link: function(scope, element, attrs, ngMessagesCtrl, $transclude) { + var commentNode = element[0]; + + var records; + var staticExp = attrs.ngMessage || attrs.when; + var dynamicExp = attrs.ngMessageExp || attrs.whenExp; + var assignRecords = function(items) { + records = items + ? (isArray(items) + ? items + : items.split(/[\s,]+/)) + : null; + ngMessagesCtrl.reRender(); + }; + + if (dynamicExp) { + assignRecords(scope.$eval(dynamicExp)); + scope.$watchCollection(dynamicExp, assignRecords); + } else { + assignRecords(staticExp); } - ngMessages.registerMessage(index, { - type: $attrs.ngMessage || $attrs.when, + var currentElement, messageCtrl; + ngMessagesCtrl.register(commentNode, messageCtrl = { + test: function(name) { + return contains(records, name); + }, attach: function() { - if (!element) { - $transclude($scope, function(clone) { - $animate.enter(clone, null, $element); - element = clone; + if (!currentElement) { + $transclude(scope, function(elm) { + $animate.enter(elm, null, element); + currentElement = elm; + + // Each time we attach this node to a message we get a new id that we can match + // when we are destroying the node later. + var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId(); + + // in the event that the parent element is destroyed + // by any other structural directive then it's time + // to deregister the message from the controller + currentElement.on('$destroy', function() { + if (currentElement && currentElement.$$attachId === $$attachId) { + ngMessagesCtrl.deregister(commentNode); + messageCtrl.detach(); + } + }); }); } }, - detach: function(now) { - if (element) { - $animate.leave(element); - element = null; + detach: function() { + if (currentElement) { + var elm = currentElement; + currentElement = null; + $animate.leave(elm); } } }); } }; - }]); + }]; + + function contains(collection, key) { + if (collection) { + return isArray(collection) + ? collection.indexOf(key) >= 0 + : collection.hasOwnProperty(key); + } + } +} })(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-mocks.js b/xstatic/pkg/angular/data/angular-mocks.js index dbfd1df..ab2ba68 100644 --- a/xstatic/pkg/angular/data/angular-mocks.js +++ b/xstatic/pkg/angular/data/angular-mocks.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) { @@ -64,10 +64,9 @@ angular.mock.$Browser = function() { return listener; }; + self.$$applicationDestroyed = angular.noop; self.$$checkUrlChange = angular.noop; - self.cookieHash = {}; - self.lastCookieHash = {}; self.deferredFns = []; self.deferredNextId = 0; @@ -95,7 +94,7 @@ angular.mock.$Browser = function() { if (fn.id === deferId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { self.deferredFns.splice(fnIndex, 1); return true; } @@ -147,11 +146,6 @@ angular.mock.$Browser.prototype = { }); }, - addPollFn: function(pollFn) { - this.pollFns.push(pollFn); - return pollFn; - }, - url: function(url, replace, state) { if (angular.isUndefined(state)) { state = null; @@ -170,25 +164,6 @@ angular.mock.$Browser.prototype = { return this.$$state; }, - cookies: function(name, value) { - if (name) { - if (angular.isUndefined(value)) { - delete this.cookieHash[name]; - } else { - if (angular.isString(value) && //strings only - value.length <= 4096) { //strict cookie storage limits - this.cookieHash[name] = value; - } - } - } else { - if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); - } - return this.cookieHash; - } - }, - notifyWhenNoOutstandingRequests: function(fn) { fn(); } @@ -458,6 +433,7 @@ angular.mock.$LogProvider = function() { * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. * @returns {promise} A promise which will be notified on each iteration. */ angular.mock.$IntervalProvider = function() { @@ -468,13 +444,17 @@ angular.mock.$IntervalProvider = function() { now = 0; var $interval = function(fn, delay, count, invokeApply) { - var iteration = 0, + var hasParams = arguments.length > 4, + args = hasParams ? Array.prototype.slice.call(arguments, 4) : [], + iteration = 0, skipApply = (angular.isDefined(invokeApply) && !invokeApply), deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise; count = (angular.isDefined(count)) ? count : 0; - promise.then(null, null, fn); + promise.then(null, null, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); promise.$$intervalId = nextRepeatId; @@ -489,7 +469,7 @@ angular.mock.$IntervalProvider = function() { if (fn.id === promise.$$intervalId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { repeatFns.splice(fnIndex, 1); } } @@ -531,7 +511,7 @@ angular.mock.$IntervalProvider = function() { if (fn.id === promise.$$intervalId) fnIndex = index; }); - if (fnIndex !== undefined) { + if (angular.isDefined(fnIndex)) { repeatFns[fnIndex].deferred.reject('canceled'); repeatFns.splice(fnIndex, 1); return true; @@ -581,20 +561,20 @@ function jsonStringToDate(string) { tzHour = 0, tzMin = 0; if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); } - date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); - date.setUTCHours(int(match[4] || 0) - tzHour, - int(match[5] || 0) - tzMin, - int(match[6] || 0), - int(match[7] || 0)); + date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + date.setUTCHours(toInt(match[4] || 0) - tzHour, + toInt(match[5] || 0) - tzMin, + toInt(match[6] || 0), + toInt(match[7] || 0)); return date; } return string; } -function int(str) { +function toInt(str) { return parseInt(str, 10); } @@ -606,8 +586,9 @@ function padNumber(num, digits, trim) { } num = '' + num; while (num.length < digits) num = '0' + num; - if (trim) + if (trim) { num = num.substr(num.length - digits); + } return neg + num; } @@ -657,11 +638,12 @@ angular.mock.TzDate = function(offset, timestamp) { self.origDate = jsonStringToDate(timestamp); timestamp = self.origDate.getTime(); - if (isNaN(timestamp)) + if (isNaN(timestamp)) { throw { name: "Illegal Argument", message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" }; + } } else { self.origDate = new Date(timestamp); } @@ -776,40 +758,152 @@ angular.mock.TzDate = function(offset, timestamp) { angular.mock.TzDate.prototype = Date.prototype; /* jshint +W101 */ + +/** + * @ngdoc service + * @name $animate + * + * @description + * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods + * for testing animations. + */ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) .config(['$provide', function($provide) { - var reflowQueue = []; - $provide.value('$$animateReflow', function(fn) { - var index = reflowQueue.length; - reflowQueue.push(fn); - return function cancel() { - reflowQueue.splice(index, 1); + $provide.factory('$$forceReflow', function() { + function reflowFn() { + reflowFn.totalReflows++; + } + reflowFn.totalReflows = 0; + return reflowFn; + }); + + $provide.factory('$$animateAsyncRun', function() { + var queue = []; + var queueFn = function() { + return function(fn) { + queue.push(fn); + }; + }; + queueFn.flush = function() { + if (queue.length === 0) return false; + + for (var i = 0; i < queue.length; i++) { + queue[i](); + } + queue = []; + + return true; }; + return queueFn; }); - $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', - function($delegate, $$asyncCallback, $timeout, $browser) { + $provide.decorator('$$animateJs', ['$delegate', function($delegate) { + var runners = []; + + var animateJsConstructor = function() { + var animator = $delegate.apply($delegate, arguments); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } + return animator; + }; + + animateJsConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateJsConstructor; + }]); + + $provide.decorator('$animateCss', ['$delegate', function($delegate) { + var runners = []; + + var animateCssConstructor = function(element, options) { + var animator = $delegate(element, options); + runners.push(animator); + return animator; + }; + + animateCssConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateCssConstructor; + }]); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$animateCss', '$$animateJs', + '$$forceReflow', '$$animateAsyncRun', '$rootScope', + function($delegate, $timeout, $browser, $$rAF, $animateCss, $$animateJs, + $$forceReflow, $$animateAsyncRun, $rootScope) { var animate = { queue: [], cancel: $delegate.cancel, - enabled: $delegate.enabled, - triggerCallbackEvents: function() { - $$asyncCallback.flush(); + on: $delegate.on, + off: $delegate.off, + pin: $delegate.pin, + get reflows() { + return $$forceReflow.totalReflows; }, - triggerCallbackPromise: function() { - $timeout.flush(0); - }, - triggerCallbacks: function() { - this.triggerCallbackEvents(); - this.triggerCallbackPromise(); + enabled: $delegate.enabled, + /** + * @ngdoc method + * @name $animate#closeAndFlush + * @description + * + * This method will close all pending animations (both {@link ngAnimate#javascript-based-animations Javascript} + * and {@link ngAnimate.$animateCss CSS}) and it will also flush any remaining animation frames and/or callbacks. + */ + closeAndFlush: function() { + // we allow the flush command to swallow the errors + // because depending on whether CSS or JS animations are + // used, there may not be a RAF flush. The primary flush + // at the end of this function must throw an exception + // because it will track if there were pending animations + this.flush(true); + $animateCss.$closeAndFlush(); + $$animateJs.$closeAndFlush(); + this.flush(); }, - triggerReflow: function() { - angular.forEach(reflowQueue, function(fn) { - fn(); - }); - reflowQueue = []; + /** + * @ngdoc method + * @name $animate#flush + * @description + * + * This method is used to flush the pending callbacks and animation frames to either start + * an animation or conclude an animation. Note that this will not actually close an + * actively running animation (see {@link ngMock.$animate#closeAndFlush `closeAndFlush()`} for that). + */ + flush: function(hideErrors) { + $rootScope.$digest(); + + var doNextRun, somethingFlushed = false; + do { + doNextRun = false; + + if ($$rAF.queue.length) { + $$rAF.flush(); + doNextRun = somethingFlushed = true; + } + + if ($$animateAsyncRun.flush()) { + doNextRun = somethingFlushed = true; + } + } while (doNextRun); + + if (!somethingFlushed && !hideErrors) { + throw new Error('No pending animations ready to be closed or flushed'); + } + + $rootScope.$digest(); } }; @@ -1026,7 +1120,7 @@ angular.mock.dump = function(object) { $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { $scope.status = ''; }).error(function() { - $scope.status = 'ERROR!'; + $scope.status = 'Failed...'; }); }; } @@ -1107,7 +1201,7 @@ angular.mock.dump = function(object) { $httpBackend.flush(); $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { - // check if the header was send, if it wasn't the expectation won't + // check if the header was sent, if it wasn't the expectation won't // match the request and the test will fail return headers['Authorization'] == 'xxx'; }).respond(201, ''); @@ -1154,7 +1248,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } // TODO(vojta): change params to: method, url, data, headers, callback - function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) { + var xhr = new MockXhr(), expectation = expectations[0], wasExpected = false; @@ -1191,14 +1286,16 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { } if (expectation && expectation.match(method, url)) { - if (!expectation.matchData(data)) + if (!expectation.matchData(data)) { throw new Error('Expected ' + expectation + ' with different data\n' + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + } - if (!expectation.matchHeaders(headers)) + if (!expectation.matchHeaders(headers)) { throw new Error('Expected ' + expectation + ' with different headers\n' + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); + } expectations.shift(); @@ -1216,7 +1313,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { // if $browser specified, we do auto flush all requests ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); } else if (definition.passThrough) { - $delegate(method, url, data, callback, headers, timeout, withCredentials); + $delegate(method, url, data, callback, headers, timeout, withCredentials, responseType); } else throw new Error('No response defined !'); return; } @@ -1234,8 +1331,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header @@ -1280,8 +1377,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1294,8 +1391,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1308,8 +1405,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1322,8 +1419,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1338,8 +1435,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives * data string and returns true if the data is as expected. * @param {(Object|function(Object))=} headers HTTP headers. @@ -1354,8 +1451,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1370,8 +1467,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1409,8 +1506,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1423,8 +1520,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1437,8 +1534,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in @@ -1451,8 +1548,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1468,8 +1565,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1485,8 +1582,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that * receives data string and returns true if the data is as expected, or Object if request body * is in JSON format. @@ -1502,8 +1599,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives an url + * and returns true if the url matches the current definition. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched * request is handled. You can save this object for later use and invoke `respond` again in * order to change how a matched request is handled. @@ -1759,47 +1856,33 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $ }]; angular.mock.$RAFDecorator = ['$delegate', function($delegate) { - var queue = []; var rafFn = function(fn) { - var index = queue.length; - queue.push(fn); + var index = rafFn.queue.length; + rafFn.queue.push(fn); return function() { - queue.splice(index, 1); + rafFn.queue.splice(index, 1); }; }; + rafFn.queue = []; rafFn.supported = $delegate.supported; rafFn.flush = function() { - if (queue.length === 0) { + if (rafFn.queue.length === 0) { throw new Error('No rAF callbacks present'); } - var length = queue.length; + var length = rafFn.queue.length; for (var i = 0; i < length; i++) { - queue[i](); + rafFn.queue[i](); } - queue = []; + rafFn.queue = rafFn.queue.slice(i); }; return rafFn; }]; -angular.mock.$AsyncCallbackDecorator = ['$delegate', function($delegate) { - var callbacks = []; - var addFn = function(fn) { - callbacks.push(fn); - }; - addFn.flush = function() { - angular.forEach(callbacks, function(fn) { - fn(); - }); - callbacks = []; - }; - return addFn; -}]; - /** * */ @@ -1833,19 +1916,19 @@ angular.mock.$RootElementProvider = function() { * * // Controller definition ... * - * myMod.controller('MyDirectiveController', ['log', function($log) { + * myMod.controller('MyDirectiveController', ['$log', function($log) { * $log.info(this.name); - * })]; + * }]); * * * // In a test ... * * describe('myDirectiveController', function() { * it('should write the bound name to the log', inject(function($controller, $log) { - * var ctrl = $controller('MyDirective', { /* no locals */ }, { name: 'Clark Kent' }); + * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' }); * expect(ctrl.name).toEqual('Clark Kent'); * expect($log.info.logs).toEqual(['Clark Kent']); - * }); + * })); * }); * * ``` @@ -1906,7 +1989,6 @@ angular.module('ngMock', ['ng']).provider({ }).config(['$provide', function($provide) { $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); - $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); $provide.decorator('$controller', angular.mock.$ControllerDecorator); }]); @@ -1985,8 +2067,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current definition. @@ -2013,8 +2095,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -2028,8 +2110,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -2043,8 +2125,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke @@ -2058,8 +2140,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2074,8 +2156,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2090,8 +2172,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for PATCH requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @param {(string|RegExp)=} data HTTP request body. * @param {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that @@ -2106,8 +2188,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp|function(string)} url HTTP url or function that receives the url - * and returns true if the url match the current definition. + * @param {string|RegExp|function(string)} url HTTP url or function that receives a url + * and returns true if the url matches the current definition. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that * control how a matched request is handled. You can save this object for later use and invoke * `respond` or `passThrough` again in order to change how a matched request is handled. @@ -2244,8 +2326,6 @@ if (window.jasmine || window.mocha) { if (injector) { injector.get('$rootElement').off(); - var $browser = injector.get('$browser'); - if ($browser.pollFns) $browser.pollFns.length = 0; } // clean up jquery's fragment cache @@ -2277,8 +2357,9 @@ if (window.jasmine || window.mocha) { * @param {...(string|Function|Object)} fns any number of modules which are represented as string * aliases or as anonymous module initialization functions. The modules are used to * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an - * object literal is passed they will be registered as values in the module, the key being - * the module name and the value being what is returned. + * object literal is passed each key-value pair will be registered on the module via + * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate + * with the value on the injector. */ window.module = angular.mock.module = function() { var moduleFns = Array.prototype.slice.call(arguments, 0); diff --git a/xstatic/pkg/angular/data/angular-route.js b/xstatic/pkg/angular/data/angular-route.js index 6b54cb6..c768251 100644 --- a/xstatic/pkg/angular/data/angular-route.js +++ b/xstatic/pkg/angular/data/angular-route.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -78,8 +78,8 @@ function $RouteProvider() { * - `controller` – `{(string|function()=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. - * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be - * published to scope under the `controllerAs` name. + * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. + * If present, the controller will be published to scope under the `controllerAs` name. * - `template` – `{string=|function()=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. @@ -206,9 +206,9 @@ function $RouteProvider() { path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; + .replace(/(\/)?:(\w+)(\*\?|[\?\*])?/g, function(_, slash, key, option) { + var optional = (option === '?' || option === '*?') ? '?' : null; + var star = (option === '*' || option === '*?') ? '*' : null; keys.push({ name: key, optional: !!optional }); slash = slash || ''; return '' @@ -468,10 +468,18 @@ function $RouteProvider() { */ reload: function() { forceReload = true; + + var fakeLocationEvent = { + defaultPrevented: false, + preventDefault: function fakePreventDefault() { + this.defaultPrevented = true; + forceReload = false; + } + }; + $rootScope.$evalAsync(function() { - // Don't support cancellation of a reload for now... - prepareRoute(); - commitRoute(); + prepareRoute(fakeLocationEvent); + if (!fakeLocationEvent.defaultPrevented) commitRoute(); }); }, @@ -609,8 +617,8 @@ function $RouteProvider() { return $q.all(locals); } }). - // after route change then(function(locals) { + // after route change if (nextRoute == $route.current) { if (nextRoute) { nextRoute.locals = locals; @@ -796,7 +804,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); } .view-animate.ng-enter, .view-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; display:block; diff --git a/xstatic/pkg/angular/data/angular-sanitize.js b/xstatic/pkg/angular/data/angular-sanitize.js index e128391..8ebce08 100644 --- a/xstatic/pkg/angular/data/angular-sanitize.js +++ b/xstatic/pkg/angular/data/angular-sanitize.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -211,10 +211,11 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a // SVG Elements // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements -var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," + - "desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," + - "line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," + - "stop,svg,switch,text,title,tspan,use"); +// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. +// They can potentially allow for arbitrary javascript to be executed. See #11290 +var svgElements = makeMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan,use"); // Special Elements (can contain anything) var specialElements = makeMap("script,style"); @@ -232,36 +233,37 @@ var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href"); var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + - 'scope,scrolling,shape,size,span,start,summary,target,title,type,' + + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + 'valign,value,vspace,width'); // SVG attributes (without "id" and "name" attributes) // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + - 'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + - 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + - 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + - 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + - 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + - 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + - 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + - 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + - 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + - 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + - 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + - 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + - 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + - 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + - 'zoomAndPan'); + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); var validAttrs = angular.extend({}, uriAttrs, svgAttrs, htmlAttrs); -function makeMap(str) { +function makeMap(str, lowercaseKeys) { var obj = {}, items = str.split(','), i; - for (i = 0; i < items.length; i++) obj[items[i]] = true; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; + } return obj; } @@ -389,8 +391,9 @@ function htmlParser(html, handler) { unary = voidElements[tagName] || !!unary; - if (!unary) + if (!unary) { stack.push(tagName); + } var attrs = {}; @@ -409,11 +412,12 @@ function htmlParser(html, handler) { function parseEndTag(tag, tagName) { var pos = 0, i; tagName = angular.lowercase(tagName); - if (tagName) + if (tagName) { // Find the closest opened tag of the same type - for (pos = stack.length - 1; pos >= 0; pos--) - if (stack[pos] == tagName) - break; + for (pos = stack.length - 1; pos >= 0; pos--) { + if (stack[pos] == tagName) break; + } + } if (pos >= 0) { // Close all the open elements, up the stack @@ -627,7 +631,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); */ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/i, + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, MAILTO_REGEXP = /^mailto:/i; return function(text, target) { diff --git a/xstatic/pkg/angular/data/angular-touch.js b/xstatic/pkg/angular/data/angular-touch.js index d9a43cc..d5a4ee7 100644 --- a/xstatic/pkg/angular/data/angular-touch.js +++ b/xstatic/pkg/angular/data/angular-touch.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) {'use strict'; @@ -43,8 +43,7 @@ function nodeName_(element) { * * Requires the {@link ngTouch `ngTouch`} module to be installed. * - * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by - * `ngCarousel` in a separate component. + * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`. * * # Usage * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element @@ -105,7 +104,8 @@ ngTouch.factory('$swipe', [function() { * `$swipe` will listen for `mouse` and `touch` events. * * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end` - * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`. + * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }` and the raw + * `event`. `cancel` receives the raw `event` as its single parameter. * * `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is * watching for `touchmove` or `mousemove` events. These events are ignored until the total @@ -263,7 +263,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', // double-tapping, and then fire a click event. // // This delay sucks and makes mobile apps feel unresponsive. - // So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when + // So we detect touchstart, touchcancel and touchend ourselves and determine when // the user has tapped on something. // // What happens when the browser then generates a click event? @@ -275,7 +275,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', // So the sequence for a tap is: // - global touchstart: Sets an "allowable region" at the point touched. // - element's touchstart: Starts a touch - // (- touchmove or touchcancel ends the touch, no click follows) + // (- touchcancel ends the touch, no click follows) // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold // too long) and fires the user's tap handler. The touchend also calls preventGhostClick(). // - preventGhostClick() removes the allowable region the global touchstart created. @@ -426,10 +426,6 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', touchStartY = e.clientY; }); - element.on('touchmove', function(event) { - resetState(); - }); - element.on('touchcancel', function(event) { resetState(); }); diff --git a/xstatic/pkg/angular/data/angular.js b/xstatic/pkg/angular/data/angular.js index 7ab67fb..ea563c0 100644 --- a/xstatic/pkg/angular/data/angular.js +++ b/xstatic/pkg/angular/data/angular.js @@ -1,6 +1,6 @@ /** - * @license AngularJS v1.3.18 - * (c) 2010-2014 Google, Inc. http://angularjs.org + * @license AngularJS v1.4.10 + * (c) 2010-2015 Google, Inc. http://angularjs.org * License: MIT */ (function(window, document, undefined) {'use strict'; @@ -38,28 +38,33 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; return function() { - var code = arguments[0], - prefix = '[' + (module ? module + ':' : '') + code + '] ', - template = arguments[1], - templateArgs = arguments, + var SKIP_INDEXES = 2; - message, i; + var templateArgs = arguments, + code = templateArgs[0], + message = '[' + (module ? module + ':' : '') + code + '] ', + template = templateArgs[1], + paramPrefix, i; - message = prefix + template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), arg; + message += template.replace(/\{\d+\}/g, function(match) { + var index = +match.slice(1, -1), + shiftedIndex = index + SKIP_INDEXES; - if (index + 2 < templateArgs.length) { - return toDebugString(templateArgs[index + 2]); + if (shiftedIndex < templateArgs.length) { + return toDebugString(templateArgs[shiftedIndex]); } + return match; }); - message = message + '\nhttp://errors.angularjs.org/1.3.18/' + + message += '\nhttp://errors.angularjs.org/1.4.10/' + (module ? module + '/' : '') + code; - for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + - encodeURIComponent(toDebugString(arguments[i])); + + for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + + encodeURIComponent(toDebugString(templateArgs[i])); } + return new ErrorConstructor(message); }; } @@ -86,20 +91,21 @@ function minErr(module, ErrorConstructor) { nodeName_: true, isArrayLike: true, forEach: true, - sortedKeys: true, forEachSorted: true, reverseParams: true, nextUid: true, setHashKey: true, extend: true, - int: true, + toInt: true, inherit: true, + merge: true, noop: true, identity: true, valueFn: true, isUndefined: true, isDefined: true, isObject: true, + isBlankObject: true, isString: true, isNumber: true, isDate: true, @@ -123,12 +129,15 @@ function minErr(module, ErrorConstructor) { shallowCopy: true, equals: true, csp: true, + jq: true, concat: true, sliceArgs: true, bind: true, toJsonReplacer: true, toJson: true, fromJson: true, + convertTimezoneToLocal: true, + timezoneToOffset: true, startingTag: true, tryDecodeURIComponent: true, parseKeyValue: true, @@ -236,6 +245,7 @@ var splice = [].splice, push = [].push, toString = Object.prototype.toString, + getPrototypeOf = Object.getPrototypeOf, ngMinErr = minErr('ng'), /** @name angular */ @@ -257,20 +267,25 @@ msie = document.documentMode; * String ...) */ function isArrayLike(obj) { - if (obj == null || isWindow(obj)) { - return false; - } + + // `null`, `undefined` and `window` are not array-like + if (obj == null || isWindow(obj)) return false; + + // arrays, strings and jQuery/jqLite objects are array like + // * jqLite is either the jQuery or jqLite constructor function + // * we have to check the existance of jqLite first as this method is called + // via the forEach method when constructing the jqLite object in the first place + if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; // Support: iOS 8.2 (not reproducible in simulator) // "length" in obj used to prevent JIT error (gh-11508) var length = "length" in Object(obj) && obj.length; - if (obj.nodeType === NODE_TYPE_ELEMENT && length) { - return true; - } + // NodeList objects (with `item` method) and + // other objects with suitable length characteristics are array-like + return isNumber(length) && + (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item == 'function'); - return isString(obj) || isArray(obj) || length === 0 || - typeof length === 'number' && length > 0 && (length - 1) in obj; } /** @@ -290,7 +305,7 @@ function isArrayLike(obj) { * * Unlike ES262's * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), - * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just + * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just * return the value provided. * ```js @@ -328,23 +343,32 @@ function forEach(obj, iterator, context) { } } else if (obj.forEach && obj.forEach !== forEach) { obj.forEach(iterator, context, obj); - } else { + } else if (isBlankObject(obj)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in obj) { + iterator.call(context, obj[key], key, obj); + } + } else if (typeof obj.hasOwnProperty === 'function') { + // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed for (key in obj) { if (obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } + } else { + // Slow path for objects which do not have a method `hasOwnProperty` + for (key in obj) { + if (hasOwnProperty.call(obj, key)) { + iterator.call(context, obj[key], key, obj); + } + } } } return obj; } -function sortedKeys(obj) { - return Object.keys(obj).sort(); -} - function forEachSorted(obj, iterator, context) { - var keys = sortedKeys(obj); + var keys = Object.keys(obj).sort(); for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } @@ -358,7 +382,7 @@ function forEachSorted(obj, iterator, context) { * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value); }; + return function(value, key) {iteratorFn(key, value);}; } /** @@ -389,6 +413,41 @@ function setHashKey(obj, h) { } } + +function baseExtend(dst, objs, deep) { + var h = dst.$$hashKey; + + for (var i = 0, ii = objs.length; i < ii; ++i) { + var obj = objs[i]; + if (!isObject(obj) && !isFunction(obj)) continue; + var keys = Object.keys(obj); + for (var j = 0, jj = keys.length; j < jj; j++) { + var key = keys[j]; + var src = obj[key]; + + if (deep && isObject(src)) { + if (isDate(src)) { + dst[key] = new Date(src.valueOf()); + } else if (isRegExp(src)) { + dst[key] = new RegExp(src); + } else if (src.nodeName) { + dst[key] = src.cloneNode(true); + } else if (isElement(src)) { + dst[key] = src.clone(); + } else { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } + } else { + dst[key] = src; + } + } + } + + setHashKey(dst, h); + return dst; +} + /** * @ngdoc function * @name angular.extend @@ -399,31 +458,44 @@ function setHashKey(obj, h) { * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so * by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`. - * Note: Keep in mind that `angular.extend` does not support recursive merge (deep copy). + * + * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use + * {@link angular.merge} for this. * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst) { - var h = dst.$$hashKey; + return baseExtend(dst, slice.call(arguments, 1), false); +} - for (var i = 1, ii = arguments.length; i < ii; i++) { - var obj = arguments[i]; - if (obj) { - var keys = Object.keys(obj); - for (var j = 0, jj = keys.length; j < jj; j++) { - var key = keys[j]; - dst[key] = obj[key]; - } - } - } - setHashKey(dst, h); - return dst; +/** +* @ngdoc function +* @name angular.merge +* @module ng +* @kind function +* +* @description +* Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s) +* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so +* by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`. +* +* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source +* objects, performing a deep copy. +* +* @param {Object} dst Destination object. +* @param {...Object} src Source object(s). +* @returns {Object} Reference to `dst`. +*/ +function merge(dst) { + return baseExtend(dst, slice.call(arguments, 1), true); } -function int(str) { + + +function toInt(str) { return parseInt(str, 10); } @@ -476,6 +548,11 @@ identity.$inject = []; function valueFn(value) {return function() {return value;};} +function hasCustomToString(obj) { + return isFunction(obj.toString) && obj.toString !== toString; +} + + /** * @ngdoc function * @name angular.isUndefined @@ -526,6 +603,16 @@ function isObject(value) { /** + * Determine if a value is an object with a null prototype + * + * @returns {boolean} True if `value` is an `Object` with a null prototype + */ +function isBlankObject(value) { + return value !== null && typeof value === 'object' && !getPrototypeOf(value); +} + + +/** * @ngdoc function * @name angular.isString * @module ng @@ -661,6 +748,12 @@ function isPromiseLike(obj) { } +var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +function isTypedArray(value) { + return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); +} + + var trim = function(value) { return isString(value) ? value.trim() : value; }; @@ -697,9 +790,10 @@ function isElement(node) { * @returns {object} in the form of {key1:true, key2:true, ...} */ function makeMap(str) { - var obj = {}, items = str.split(","), i; - for (i = 0; i < items.length; i++) + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) { obj[items[i]] = true; + } return obj; } @@ -714,9 +808,10 @@ function includes(array, obj) { function arrayRemove(array, value) { var index = array.indexOf(value); - if (index >= 0) + if (index >= 0) { array.splice(index, 1); - return value; + } + return index; } /** @@ -777,77 +872,113 @@ function arrayRemove(array, value) { </file> </example> */ -function copy(source, destination, stackSource, stackDest) { - if (isWindow(source) || isScope(source)) { - throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); - } +function copy(source, destination) { + var stackSource = []; + var stackDest = []; - if (!destination) { - destination = source; - if (source) { - if (isArray(source)) { - destination = copy(source, [], stackSource, stackDest); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - destination.lastIndex = source.lastIndex; - } else if (isObject(source)) { - var emptyObject = Object.create(Object.getPrototypeOf(source)); - destination = copy(source, emptyObject, stackSource, stackDest); - } + if (destination) { + if (isTypedArray(destination)) { + throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated."); + } + if (source === destination) { + throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); } - } else { - if (source === destination) throw ngMinErr('cpi', - "Can't copy! Source and destination are identical."); - stackSource = stackSource || []; - stackDest = stackDest || []; + // Empty the destination object + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + if (key !== '$$hashKey') { + delete destination[key]; + } + }); + } - if (isObject(source)) { - var index = stackSource.indexOf(source); - if (index !== -1) return stackDest[index]; + stackSource.push(source); + stackDest.push(destination); + return copyRecurse(source, destination); + } - stackSource.push(source); - stackDest.push(destination); - } + return copyElement(source); - var result; + function copyRecurse(source, destination) { + var h = destination.$$hashKey; + var result, key; if (isArray(source)) { - destination.length = 0; - for (var i = 0; i < source.length; i++) { - result = copy(source[i], null, stackSource, stackDest); - if (isObject(source[i])) { - stackSource.push(source[i]); - stackDest.push(result); - } - destination.push(result); + for (var i = 0, ii = source.length; i < ii; i++) { + destination.push(copyElement(source[i])); } - } else { - var h = destination.$$hashKey; - if (isArray(destination)) { - destination.length = 0; - } else { - forEach(destination, function(value, key) { - delete destination[key]; - }); + } else if (isBlankObject(source)) { + // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty + for (key in source) { + destination[key] = copyElement(source[key]); } - for (var key in source) { + } else if (source && typeof source.hasOwnProperty === 'function') { + // Slow path, which must rely on hasOwnProperty + for (key in source) { if (source.hasOwnProperty(key)) { - result = copy(source[key], null, stackSource, stackDest); - if (isObject(source[key])) { - stackSource.push(source[key]); - stackDest.push(result); - } - destination[key] = result; + destination[key] = copyElement(source[key]); + } + } + } else { + // Slowest path --- hasOwnProperty can't be called as a method + for (key in source) { + if (hasOwnProperty.call(source, key)) { + destination[key] = copyElement(source[key]); } } - setHashKey(destination,h); + } + setHashKey(destination, h); + return destination; + } + + function copyElement(source) { + // Simple values + if (!isObject(source)) { + return source; + } + + // Already copied values + var index = stackSource.indexOf(source); + if (index !== -1) { + return stackDest[index]; } + if (isWindow(source) || isScope(source)) { + throw ngMinErr('cpws', + "Can't copy! Making copies of Window or Scope instances is not supported."); + } + + var needsRecurse = false; + var destination; + + if (isArray(source)) { + destination = []; + needsRecurse = true; + } else if (isTypedArray(source)) { + destination = new source.constructor(source); + } else if (isDate(source)) { + destination = new Date(source.getTime()); + } else if (isRegExp(source)) { + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; + } else if (isBlob(source)) { + destination = new source.constructor([source], {type: source.type}); + } else if (isFunction(source.cloneNode)) { + destination = source.cloneNode(true); + } else { + destination = Object.create(getPrototypeOf(source)); + needsRecurse = true; + } + + stackSource.push(source); + stackDest.push(destination); + + return needsRecurse + ? copyRecurse(source, destination) + : destination; } - return destination; } /** @@ -928,16 +1059,16 @@ function equals(o1, o2) { } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = {}; + keySet = createMap(); for (key in o1) { if (key.charAt(0) === '$' || isFunction(o1[key])) continue; if (!equals(o1[key], o2[key])) return false; keySet[key] = true; } for (key in o2) { - if (!keySet.hasOwnProperty(key) && + if (!(key in keySet) && key.charAt(0) !== '$' && - o2[key] !== undefined && + isDefined(o2[key]) && !isFunction(o2[key])) return false; } return true; @@ -948,25 +1079,93 @@ function equals(o1, o2) { } var csp = function() { - if (isDefined(csp.isActive_)) return csp.isActive_; + if (!isDefined(csp.rules)) { + + + var ngCspElement = (document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); + + if (ngCspElement) { + var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || + ngCspElement.getAttribute('data-ng-csp'); + csp.rules = { + noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1), + noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1) + }; + } else { + csp.rules = { + noUnsafeEval: noUnsafeEval(), + noInlineStyle: false + }; + } + } - var active = !!(document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); + return csp.rules; - if (!active) { + function noUnsafeEval() { try { /* jshint -W031, -W054 */ new Function(''); /* jshint +W031, +W054 */ + return false; } catch (e) { - active = true; + return true; } } - - return (csp.isActive_ = active); }; +/** + * @ngdoc directive + * @module ng + * @name ngJq + * + * @element ANY + * @param {string=} ngJq the name of the library available under `window` + * to be used for angular.element + * @description + * Use this directive to force the angular.element library. This should be + * used to force either jqLite by leaving ng-jq blank or setting the name of + * the jquery variable under window (eg. jQuery). + * + * Since angular looks for this directive when it is loaded (doesn't wait for the + * DOMContentLoaded event), it must be placed on an element that comes before the script + * which loads angular. Also, only the first instance of `ng-jq` will be used and all + * others ignored. + * + * @example + * This example shows how to force jqLite using the `ngJq` directive to the `html` tag. + ```html + <!doctype html> + <html ng-app ng-jq> + ... + ... + </html> + ``` + * @example + * This example shows how to use a jQuery based library of a different name. + * The library name must be available at the top most 'window'. + ```html + <!doctype html> + <html ng-app ng-jq="jQueryLib"> + ... + ... + </html> + ``` + */ +var jq = function() { + if (isDefined(jq.name_)) return jq.name_; + var el; + var i, ii = ngAttrPrefixes.length, prefix, name; + for (i = 0; i < ii; ++i) { + prefix = ngAttrPrefixes[i]; + if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { + name = el.getAttribute(prefix + 'jq'); + break; + } + } + return (jq.name_ = name); +}; function concat(array1, array2, index) { return array1.concat(slice.call(array2, index)); @@ -1050,7 +1249,7 @@ function toJsonReplacer(key, value) { * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; + if (isUndefined(obj)) return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } @@ -1077,6 +1276,30 @@ function fromJson(json) { } +var ALL_COLONS = /:/g; +function timezoneToOffset(timezone, fallback) { + // IE/Edge do not "understand" colon (`:`) in timezone + timezone = timezone.replace(ALL_COLONS, ''); + var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; + return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; +} + + +function addDateMinutes(date, minutes) { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + minutes); + return date; +} + + +function convertTimezoneToLocal(date, timezone, reverse) { + reverse = reverse ? -1 : 1; + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); +} + + /** * @returns {string} Returns the string representation of the element. */ @@ -1092,7 +1315,7 @@ function startingTag(element) { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); + replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1124,13 +1347,19 @@ function tryDecodeURIComponent(value) { * @returns {Object.<string,boolean|Array>} */ function parseKeyValue(/**string*/keyValue) { - var obj = {}, key_value, key; + var obj = {}; forEach((keyValue || "").split('&'), function(keyValue) { + var splitPoint, key, val; if (keyValue) { - key_value = keyValue.replace(/\+/g,'%20').split('='); - key = tryDecodeURIComponent(key_value[0]); + key = keyValue = keyValue.replace(/\+/g,'%20'); + splitPoint = keyValue.indexOf('='); + if (splitPoint !== -1) { + key = keyValue.substring(0, splitPoint); + val = keyValue.substring(splitPoint + 1); + } + key = tryDecodeURIComponent(key); if (isDefined(key)) { - var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; + val = isDefined(val) ? tryDecodeURIComponent(val) : true; if (!hasOwnProperty.call(obj, key)) { obj[key] = val; } else if (isArray(obj[key])) { @@ -1205,10 +1434,9 @@ var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-']; function getNgAttribute(element, ngAttr) { var attr, i, ii = ngAttrPrefixes.length; - element = jqLite(element); for (i = 0; i < ii; ++i) { attr = ngAttrPrefixes[i] + ngAttr; - if (isString(attr = element.attr(attr))) { + if (isString(attr = element.getAttribute(attr))) { return attr; } } @@ -1539,7 +1767,11 @@ function bindJQuery() { } // bind to jQuery if present; - jQuery = window.jQuery; + var jqName = jq(); + jQuery = isUndefined(jqName) ? window.jQuery : // use jQuery (if present) + !jqName ? undefined : // use jqLite + window[jqName]; // use jQuery specified by `ngJq` + // Use jQuery if it exists with proper functionality, otherwise default to us. // Angular 1.2+ requires jQuery 1.7+ for on()/off() support. // Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older @@ -1643,22 +1875,24 @@ function getter(obj, path, bindFnToScope) { /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object - * @returns {jqLite} jqLite collection containing the nodes + * @returns {Array} the inputted object or a jqLite collection containing the nodes */ function getBlockNodes(nodes) { - // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original - // collection, otherwise update the original collection. + // TODO(perf): update `nodes` instead of creating a new object? var node = nodes[0]; var endNode = nodes[nodes.length - 1]; - var blockNodes = [node]; + var blockNodes; - do { - node = node.nextSibling; - if (!node) break; - blockNodes.push(node); - } while (node !== endNode); + for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { + if (blockNodes || nodes[i] !== node) { + if (!blockNodes) { + blockNodes = jqLite(slice.call(nodes, 0, i)); + } + blockNodes.push(node); + } + } - return jqLite(blockNodes); + return blockNodes || nodes; } @@ -1722,8 +1956,8 @@ function setupModuleLoader(window) { * All modules (angular core or 3rd party) that should be available to an application must be * registered using this mechanism. * - * When passed two or more arguments, a new module is created. If passed only one argument, an - * existing module (the name passed as the first argument to `module`) is retrieved. + * Passing one argument retrieves an existing {@link angular.Module}, + * whereas passing more than one argument creates a new {@link angular.Module} * * * # Module @@ -1760,7 +1994,7 @@ function setupModuleLoader(window) { * unspecified then the module is being retrieved for further configuration. * @param {Function=} configFn Optional configuration function for the module. Same as * {@link angular.Module#config Module#config()}. - * @returns {module} new module with the {@link angular.Module} api. + * @returns {angular.Module} new module with the {@link angular.Module} api. */ return function module(name, requires, configFn) { var assertNotHasOwnProperty = function(name, context) { @@ -1830,7 +2064,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#provider $provide.provider()}. */ - provider: invokeLater('$provide', 'provider'), + provider: invokeLaterAndSetModuleName('$provide', 'provider'), /** * @ngdoc method @@ -1841,7 +2075,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#factory $provide.factory()}. */ - factory: invokeLater('$provide', 'factory'), + factory: invokeLaterAndSetModuleName('$provide', 'factory'), /** * @ngdoc method @@ -1852,7 +2086,7 @@ function setupModuleLoader(window) { * @description * See {@link auto.$provide#service $provide.service()}. */ - service: invokeLater('$provide', 'service'), + service: invokeLaterAndSetModuleName('$provide', 'service'), /** * @ngdoc method @@ -1872,11 +2106,23 @@ function setupModuleLoader(window) { * @param {string} name constant name * @param {*} object Constant value. * @description - * Because the constant are fixed, they get applied before other provide methods. + * Because the constants are fixed, they get applied before other provide methods. * See {@link auto.$provide#constant $provide.constant()}. */ constant: invokeLater('$provide', 'constant', 'unshift'), + /** + * @ngdoc method + * @name angular.Module#decorator + * @module ng + * @param {string} The name of the service to decorate. + * @param {Function} This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. + * @description + * See {@link auto.$provide#decorator $provide.decorator()}. + */ + decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + /** * @ngdoc method * @name angular.Module#animation @@ -1890,7 +2136,7 @@ function setupModuleLoader(window) { * * * Defines an animation hook that can be later used with - * {@link ngAnimate.$animate $animate} service and directives that use this service. + * {@link $animate $animate} service and directives that use this service. * * ```js * module.animation('.animation-name', function($inject1, $inject2) { @@ -1909,7 +2155,7 @@ function setupModuleLoader(window) { * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ - animation: invokeLater('$animateProvider', 'register'), + animation: invokeLaterAndSetModuleName('$animateProvider', 'register'), /** * @ngdoc method @@ -1927,7 +2173,7 @@ function setupModuleLoader(window) { * (`myapp_subsection_filterx`). * </div> */ - filter: invokeLater('$filterProvider', 'register'), + filter: invokeLaterAndSetModuleName('$filterProvider', 'register'), /** * @ngdoc method @@ -1939,7 +2185,7 @@ function setupModuleLoader(window) { * @description * See {@link ng.$controllerProvider#register $controllerProvider.register()}. */ - controller: invokeLater('$controllerProvider', 'register'), + controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'), /** * @ngdoc method @@ -1952,7 +2198,7 @@ function setupModuleLoader(window) { * @description * See {@link ng.$compileProvider#directive $compileProvider.directive()}. */ - directive: invokeLater('$compileProvider', 'directive'), + directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'), /** * @ngdoc method @@ -2002,6 +2248,19 @@ function setupModuleLoader(window) { return moduleInstance; }; } + + /** + * @param {string} provider + * @param {string} method + * @returns {angular.Module} + */ + function invokeLaterAndSetModuleName(provider, method) { + return function(recipeName, factoryFunction) { + if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; + invokeQueue.push([provider, method, arguments]); + return moduleInstance; + }; + } }); }; }); @@ -2017,7 +2276,7 @@ function serializeObject(obj) { val = toJsonReplacer(key, val); if (isObject(val)) { - if (seen.indexOf(val) >= 0) return '<<already seen>>'; + if (seen.indexOf(val) >= 0) return '...'; seen.push(val); } @@ -2028,7 +2287,7 @@ function serializeObject(obj) { function toDebugString(obj) { if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { + } else if (isUndefined(obj)) { return 'undefined'; } else if (typeof obj !== 'string') { return serializeObject(obj); @@ -2039,7 +2298,6 @@ function toDebugString(obj) { /* global angularModule: true, version: true, - $LocaleProvider, $CompileProvider, htmlAnchorDirective, @@ -2056,7 +2314,6 @@ function toDebugString(obj) { ngClassDirective, ngClassEvenDirective, ngClassOddDirective, - ngCspDirective, ngCloakDirective, ngControllerDirective, ngFormDirective, @@ -2093,16 +2350,26 @@ function toDebugString(obj) { $AnchorScrollProvider, $AnimateProvider, + $CoreAnimateCssProvider, + $$CoreAnimateJsProvider, + $$CoreAnimateQueueProvider, + $$AnimateRunnerFactoryProvider, + $$AnimateAsyncRunFactoryProvider, $BrowserProvider, $CacheFactoryProvider, $ControllerProvider, $DocumentProvider, $ExceptionHandlerProvider, $FilterProvider, + $$ForceReflowProvider, $InterpolateProvider, $IntervalProvider, + $$HashMapProvider, $HttpProvider, + $HttpParamSerializerProvider, + $HttpParamSerializerJQLikeProvider, $HttpBackendProvider, + $xhrFactoryProvider, $LocationProvider, $LogProvider, $ParseProvider, @@ -2118,9 +2385,9 @@ function toDebugString(obj) { $$TestabilityProvider, $TimeoutProvider, $$RAFProvider, - $$AsyncCallbackProvider, $WindowProvider, - $$jqLiteProvider + $$jqLiteProvider, + $$CookieReaderProvider */ @@ -2129,8 +2396,9 @@ function toDebugString(obj) { * @name angular.version * @module ng * @description - * An object that contains information about the current AngularJS version. This object has the - * following properties: + * An object that contains information about the current AngularJS version. + * + * This object has the following properties: * * - `full` – `{string}` – Full version string, such as "0.9.18". * - `major` – `{number}` – Major version number, such as "0". @@ -2139,11 +2407,11 @@ function toDebugString(obj) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.3.18', // all of these placeholder strings will be replaced by grunt's + full: '1.4.10', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 3, - dot: 18, - codeName: 'collective-penmanship' + minor: 4, + dot: 10, + codeName: 'benignant-oscillation' }; @@ -2152,6 +2420,7 @@ function publishExternalAPI(angular) { 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, + 'merge': merge, 'equals': equals, 'element': jqLite, 'forEach': forEach, @@ -2181,11 +2450,6 @@ function publishExternalAPI(angular) { }); angularModule = setupModuleLoader(window); - try { - angularModule('ngLocale'); - } catch (e) { - angularModule('ngLocale', []).provider('$locale', $LocaleProvider); - } angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { @@ -2248,16 +2512,25 @@ function publishExternalAPI(angular) { $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, + $animateCss: $CoreAnimateCssProvider, + $$animateJs: $$CoreAnimateJsProvider, + $$animateQueue: $$CoreAnimateQueueProvider, + $$AnimateRunner: $$AnimateRunnerFactoryProvider, + $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, + $$forceReflow: $$ForceReflowProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, + $httpParamSerializer: $HttpParamSerializerProvider, + $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, + $xhrFactory: $xhrFactoryProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, @@ -2273,8 +2546,9 @@ function publishExternalAPI(angular) { $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, - $$asyncCallback: $$AsyncCallbackProvider, - $$jqLite: $$jqLiteProvider + $$jqLite: $$jqLiteProvider, + $$HashMap: $$HashMapProvider, + $$cookieReader: $$CookieReaderProvider }); } ]); @@ -2313,16 +2587,22 @@ function publishExternalAPI(angular) { * * If jQuery is available, `angular.element` is an alias for the * [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element` - * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite." + * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**. + * + * jqLite is a tiny, API-compatible subset of jQuery that allows + * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most + * commonly needed functionality with the goal of having a very small footprint. * - * <div class="alert alert-success">jqLite is a tiny, API-compatible subset of jQuery that allows - * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most - * commonly needed functionality with the goal of having a very small footprint.</div> + * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the + * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a + * specific version of jQuery if multiple versions exist on the page. * - * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. + * <div class="alert alert-info">**Note:** All element references in Angular are always wrapped with jQuery or + * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.</div> * - * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or - * jqLite; they are never raw DOM references.</div> + * <div class="alert alert-warning">**Note:** Keep in mind that this function will not find elements + * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)` + * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.</div> * * ## Angular's jqLite * jqLite provides only the following jQuery methods: @@ -2335,7 +2615,8 @@ function publishExternalAPI(angular) { * - [`children()`](http://api.jquery.com/children/) - Does not support selectors * - [`clone()`](http://api.jquery.com/clone/) * - [`contents()`](http://api.jquery.com/contents/) - * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'. + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. + * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing. * - [`data()`](http://api.jquery.com/data/) * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) @@ -2345,7 +2626,7 @@ function publishExternalAPI(angular) { * - [`html()`](http://api.jquery.com/html/) * - [`next()`](http://api.jquery.com/next/) - Does not support selectors * - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData - * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors + * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter * - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) @@ -2359,7 +2640,7 @@ function publishExternalAPI(angular) { * - [`text()`](http://api.jquery.com/text/) * - [`toggleClass()`](http://api.jquery.com/toggleClass/) * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces + * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2431,10 +2712,10 @@ function camelCase(name) { replace(MOZ_HACK_REGEXP, 'Moz$1'); } -var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; +var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; -var TAG_NAME_REGEXP = /<([\w:]+)/; -var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi; +var TAG_NAME_REGEXP = /<([\w:-]+)/; +var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi; var wrapMap = { 'option': [1, '<select multiple="multiple">', '</select>'], @@ -2462,6 +2743,13 @@ function jqLiteAcceptsData(node) { return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; } +function jqLiteHasData(node) { + for (var key in jqCache[node.ng339]) { + return true; + } + return false; +} + function jqLiteBuildFragment(html, context) { var tmp, tag, wrap, fragment = context.createDocumentFragment(), @@ -2514,6 +2802,24 @@ function jqLiteParseHTML(html, context) { return []; } +function jqLiteWrapNode(node, wrapper) { + var parent = node.parentNode; + + if (parent) { + parent.replaceChild(wrapper, node); + } + + wrapper.appendChild(node); +} + + +// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. +var jqLiteContains = Node.prototype.contains || function(arg) { + // jshint bitwise: false + return !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true +}; + ///////////////////////////////////////////// function JQLite(element) { if (element instanceof JQLite) { @@ -2572,17 +2878,23 @@ function jqLiteOff(element, type, fn, unsupported) { delete events[type]; } } else { - forEach(type.split(' '), function(type) { + + var removeHandler = function(type) { + var listenerFns = events[type]; if (isDefined(fn)) { - var listenerFns = events[type]; arrayRemove(listenerFns || [], fn); - if (listenerFns && listenerFns.length > 0) { - return; - } } + if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { + removeEventListenerFn(element, type, handle); + delete events[type]; + } + }; - removeEventListenerFn(element, type, handle); - delete events[type]; + forEach(type.split(' '), function(type) { + removeHandler(type); + if (MOUSE_EVENT_MAP[type]) { + removeHandler(MOUSE_EVENT_MAP[type]); + } }); } } @@ -2723,7 +3035,7 @@ function jqLiteInheritedData(element, name, value) { while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = jqLite.data(element, names[i])) !== undefined) return value; + if (isDefined(value = jqLite.data(element, names[i]))) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host @@ -2829,14 +3141,14 @@ function getBooleanAttrName(element, name) { return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; } -function getAliasedAttrName(element, name) { - var nodeName = element.nodeName; - return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; +function getAliasedAttrName(name) { + return ALIASED_ATTR[name]; } forEach({ data: jqLiteData, - removeData: jqLiteRemoveData + removeData: jqLiteRemoveData, + hasData: jqLiteHasData }, function(fn, name) { JQLite[name] = fn; }); @@ -2967,7 +3279,7 @@ forEach({ // in a way that survives minification. // jqLiteEmpty takes no arguments but is a setter. if (fn !== jqLiteEmpty && - (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) { + (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) { if (isObject(arg1)) { // we are a write, but the object properties are the key/values @@ -2988,7 +3300,7 @@ forEach({ // TODO: do we still need this? var value = fn.$dv; // Only if we have $dv do we iterate over all, otherwise it is just the first element. - var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; + var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -3037,6 +3349,9 @@ function createEventHandler(element, events) { return event.immediatePropagationStopped === true; }; + // Some events have special handlers that wrap the real handler + var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper; + // Copy event handlers in case event handlers array is modified during execution. if ((eventFnsLength > 1)) { eventFns = shallowCopy(eventFns); @@ -3044,7 +3359,7 @@ function createEventHandler(element, events) { for (var i = 0; i < eventFnsLength; i++) { if (!event.isImmediatePropagationStopped()) { - eventFns[i].call(element, event); + handlerWrapper(element, event, eventFns[i]); } } }; @@ -3055,6 +3370,22 @@ function createEventHandler(element, events) { return eventHandler; } +function defaultHandlerWrapper(element, event, handler) { + handler.call(element, event); +} + +function specialMouseHandlerWrapper(target, event, handler) { + // Refer to jQuery's implementation of mouseenter & mouseleave + // Read about mouseenter and mouseleave: + // http://www.quirksmode.org/js/events_mouse.html#link8 + var related = event.relatedTarget; + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if (!related || (related !== target && !jqLiteContains.call(target, related))) { + handler.call(target, event); + } +} + ////////////////////////////////////////// // Functions iterating traversal. // These functions chain results into a single @@ -3083,35 +3414,28 @@ forEach({ var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; var i = types.length; - while (i--) { - type = types[i]; + var addHandler = function(type, specialHandlerWrapper, noEventListener) { var eventFns = events[type]; if (!eventFns) { - events[type] = []; - - if (type === 'mouseenter' || type === 'mouseleave') { - // Refer to jQuery's implementation of mouseenter & mouseleave - // Read about mouseenter and mouseleave: - // http://www.quirksmode.org/js/events_mouse.html#link8 - - jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) { - var target = this, related = event.relatedTarget; - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if (!related || (related !== target && !target.contains(related))) { - handle(event, type); - } - }); - - } else { - if (type !== '$destroy') { - addEventListenerFn(element, type, handle); - } + eventFns = events[type] = []; + eventFns.specialHandlerWrapper = specialHandlerWrapper; + if (type !== '$destroy' && !noEventListener) { + addEventListenerFn(element, type, handle); } - eventFns = events[type]; } + eventFns.push(fn); + }; + + while (i--) { + type = types[i]; + if (MOUSE_EVENT_MAP[type]) { + addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper); + addHandler(type, undefined, true); + } else { + addHandler(type); + } } }, @@ -3146,8 +3470,9 @@ forEach({ children: function(element) { var children = []; forEach(element.childNodes, function(element) { - if (element.nodeType === NODE_TYPE_ELEMENT) + if (element.nodeType === NODE_TYPE_ELEMENT) { children.push(element); + } }); return children; }, @@ -3178,12 +3503,7 @@ forEach({ }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode).eq(0).clone()[0]; - var parent = element.parentNode; - if (parent) { - parent.replaceChild(wrapNode, element); - } - wrapNode.appendChild(element); + jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]); }, remove: jqLiteRemove, @@ -3393,6 +3713,12 @@ HashMap.prototype = { } }; +var $$HashMapProvider = [function() { + this.$get = [function() { + return HashMap; + }]; +}]; + /** * @ngdoc function * @module ng @@ -3455,7 +3781,7 @@ HashMap.prototype = { * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; +var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; @@ -3880,8 +4206,20 @@ function annotate(fn, strictDi, name) { * * Register a **service constructor**, which will be invoked with `new` to create the service * instance. - * This is short for registering a service where its provider's `$get` property is the service - * constructor function that will be used to instantiate the service instance. + * This is short for registering a service where its provider's `$get` property is a factory + * function that returns an instance instantiated by the injector from the service constructor + * function. + * + * Internally it looks a bit like this: + * + * ``` + * { + * $get: function() { + * return $injector.instantiate(constructor); + * } + * } + * ``` + * * * You should use {@link auto.$provide#service $provide.service(class)} if you define your service * as a type/class. @@ -3921,14 +4259,13 @@ function annotate(fn, strictDi, name) { * @description * * Register a **value service** with the {@link auto.$injector $injector}, such as a string, a - * number, an array, an object or a function. This is short for registering a service where its + * number, an array, an object or a function. This is short for registering a service where its * provider's `$get` property is a factory function that takes no arguments and returns the **value - * service**. + * service**. That also means it is not possible to inject other services into a value service. * * Value services are similar to constant services, except that they cannot be injected into a * module configuration function (see {@link angular.Module#config}) but they can be overridden by - * an Angular - * {@link auto.$provide#decorator decorator}. + * an Angular {@link auto.$provide#decorator decorator}. * * @param {string} name The name of the instance. * @param {*} value The value. @@ -3953,8 +4290,11 @@ function annotate(fn, strictDi, name) { * @name $provide#constant * @description * - * Register a **constant service**, such as a string, a number, an array, an object or a function, - * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be + * Register a **constant service** with the {@link auto.$injector $injector}, such as a string, + * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not + * possible to inject other services into a constant. + * + * But unlike {@link auto.$provide#value value}, a constant can be * injected into a module configuration function (see {@link angular.Module#config}) and it cannot * be overridden by an Angular {@link auto.$provide#decorator decorator}. * @@ -3982,7 +4322,7 @@ function annotate(fn, strictDi, name) { * @description * * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator - * intercepts the creation of a service, allowing it to override or modify the behaviour of the + * intercepts the creation of a service, allowing it to override or modify the behavior of the * service. The object returned by the decorator may be the original service, or a new service * object which replaces or wraps and delegates to the original service. * @@ -4038,7 +4378,7 @@ function createInjector(modulesToLoad, strictDi) { })); - forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); + forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); }); return instanceInjector; @@ -4111,6 +4451,7 @@ function createInjector(modulesToLoad, strictDi) { // Module Loading //////////////////////////////////// function loadModules(modulesToLoad) { + assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array'); var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; @@ -4281,9 +4622,10 @@ function $AnchorScrollProvider() { * @requires $rootScope * * @description - * When called, it checks the current value of {@link ng.$location#hash $location.hash()} and - * scrolls to the related element, according to the rules specified in the - * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). + * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the + * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified + * in the + * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document). * * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to * match any anchor whenever it changes. This can be disabled by calling @@ -4292,6 +4634,9 @@ function $AnchorScrollProvider() { * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a * vertical scroll-offset (either fixed or dynamic). * + * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of + * {@link ng.$location#hash $location.hash()} will be used. + * * @property {(number|function|jqLite)} yOffset * If set, specifies a vertical scroll-offset. This is often useful when there are fixed * positioned elements at the top of the page, such as navbars, headers etc. @@ -4475,8 +4820,9 @@ function $AnchorScrollProvider() { } } - function scroll() { - var hash = $location.hash(), elm; + function scroll(hash) { + hash = isString(hash) ? hash : $location.hash(); + var elm; // empty hash, scroll to the top of the page if (!hash) scrollTo(null); @@ -4510,6 +4856,159 @@ function $AnchorScrollProvider() { } 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 = function() { + this.$get = function() {}; +}; + +// this is prefixed with Core since it conflicts with +// the animateQueueProvider defined in ngAnimate/animateQueue.js +var $$CoreAnimateQueueProvider = function() { + var postDigestQueue = new HashMap(); + var postDigestElements = []; + + this.$get = ['$$AnimateRunner', '$rootScope', + function($$AnimateRunner, $rootScope) { + return { + enabled: noop, + on: noop, + off: noop, + pin: noop, + + push: function(element, event, options, domOperation) { + domOperation && domOperation(); + + options = options || {}; + options.from && element.css(options.from); + options.to && element.css(options.to); + + if (options.addClass || options.removeClass) { + addRemoveClassesPostDigest(element, options.addClass, options.removeClass); + } + + var runner = new $$AnimateRunner(); // jshint ignore:line + + // 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) { + toAdd && jqLiteAddClass(elm, toAdd); + 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 @@ -4517,20 +5016,18 @@ var $animateMinErr = minErr('$animate'); * * @description * Default implementation of $animate that doesn't perform any animations, instead just - * synchronously performs DOM - * updates and calls done() callbacks. + * synchronously performs DOM updates and resolves the returned runner promise. * - * In order to enable animations the ngAnimate module has to be loaded. + * In order to enable animations the `ngAnimate` module has to be loaded. * - * To see the functional implementation check out src/ngAnimate/animate.js + * To see the functional implementation check out `src/ngAnimate/animate.js`. */ var $AnimateProvider = ['$provide', function($provide) { + var provider = this; + this.$$registeredAnimations = Object.create(null); - this.$$selectors = {}; - - - /** + /** * @ngdoc method * @name $animateProvider#register * @@ -4539,33 +5036,43 @@ var $AnimateProvider = ['$provide', function($provide) { * animation object which contains callback functions for each event that is expected to be * animated. * - * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` - * must be called once the element animation is complete. If a function is returned then the - * animation service will use this function to cancel the animation whenever a cancel event is - * triggered. + * * `eventFn`: `function(element, ... , doneFunction, options)` + * The element to animate, the `doneFunction` and the options fed into the animation. Depending + * on the type of animation additional arguments will be injected into the animation function. The + * list below explains the function signatures for the different animation methods: * + * - setClass: function(element, addedClasses, removedClasses, doneFunction, options) + * - addClass: function(element, addedClasses, doneFunction, options) + * - removeClass: function(element, removedClasses, doneFunction, options) + * - enter, leave, move: function(element, doneFunction, options) + * - animate: function(element, fromStyles, toStyles, doneFunction, options) + * + * Make sure to trigger the `doneFunction` once the animation is fully complete. * * ```js * return { - * eventFn : function(element, done) { - * //code to run the animation - * //once complete, then run done() - * return function cancellationFunction() { - * //code to cancel the animation - * } - * } - * } + * //enter, leave, move signature + * eventFn : function(element, done, options) { + * //code to run the animation + * //once complete, then run done() + * return function endFunction(wasCancelled) { + * //code to cancel the animation + * } + * } + * } * ``` * - * @param {string} name The name of the animation. + * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to). * @param {Function} factory The factory function that will be executed to return the animation * object. */ this.register = function(name, factory) { + if (name && name.charAt(0) !== '.') { + throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name); + } + var key = name + '-animation'; - if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', - "Expecting class selector starting with '.' got '{0}'.", name); - this.$$selectors[name.substr(1)] = key; + provider.$$registeredAnimations[name.substr(1)] = key; $provide.factory(key, factory); }; @@ -4576,8 +5083,8 @@ var $AnimateProvider = ['$provide', function($provide) { * @description * Sets and/or returns the CSS class regular expression that is checked when performing * an animation. Upon bootstrap the classNameFilter value is not set at all and will - * therefore enable $animate to attempt to perform an animation on any element. - * When setting the classNameFilter value, animations will only be performed on elements + * therefore enable $animate to attempt to perform an animation on any element that is triggered. + * When setting the `classNameFilter` value, animations will only be performed on elements * that successfully match the filter expression. This in turn can boost performance * for low-powered devices as well as applications containing a lot of structural operations. * @param {RegExp=} expression The className expression which will be checked against all animations @@ -4586,102 +5093,167 @@ var $AnimateProvider = ['$provide', function($provide) { this.classNameFilter = function(expression) { if (arguments.length === 1) { this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; - } - return this.$$classNameFilter; - }; - - this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { - - var currentDefer; - - function runAnimationPostDigest(fn) { - var cancelFn, defer = $$q.defer(); - defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { - cancelFn && cancelFn(); - }; - - $rootScope.$$postDigest(function ngAnimatePostDigest() { - cancelFn = fn(function ngAnimateNotifyComplete() { - defer.resolve(); - }); - }); - - return defer.promise; - } + 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); - function resolveElementClasses(element, classes) { - var toAdd = [], toRemove = []; - - var hasClasses = createMap(); - forEach((element.attr('class') || '').split(/\s+/), function(className) { - hasClasses[className] = true; - }); - - forEach(classes, function(status, className) { - var hasClass = hasClasses[className]; - - // If the most recent class manipulation (via $animate) was to remove the class, and the - // element currently has the class, the class is scheduled for removal. Otherwise, if - // the most recent class manipulation (via $animate) was to add the class, and the - // element does not currently have the class, the class is scheduled to be added. - if (status === false && hasClass) { - toRemove.push(className); - } else if (status === true && !hasClass) { - toAdd.push(className); } - }); - - return (toAdd.length + toRemove.length) > 0 && - [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null]; - } - - function cachedClassManipulation(cache, classes, op) { - for (var i=0, ii = classes.length; i < ii; ++i) { - var className = classes[i]; - cache[className] = op; } } + return this.$$classNameFilter; + }; - function asyncPromise() { - // only serve one instance of a promise in order to save CPU cycles - if (!currentDefer) { - currentDefer = $$q.defer(); - $$asyncCallback(function() { - currentDefer.resolve(); - currentDefer = null; - }); - } - return currentDefer.promise; - } - - function applyStyles(element, options) { - if (angular.isObject(options)) { - var styles = extend(options.from || {}, options.to || {}); - element.css(styles); + 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; + } } + afterElement ? afterElement.after(element) : parentElement.prepend(element); } /** - * * @ngdoc service * @name $animate - * @description The $animate service provides rudimentary DOM manipulation functions to - * insert, remove and move elements within the DOM, as well as adding and removing classes. - * This service is the core service used by the ngAnimate $animator service which provides - * high-level animation hooks for CSS and JavaScript. + * @description The $animate service exposes a series of DOM utility methods that provide support + * for animation hooks. The default behavior is the application of DOM operations, however, + * when an animation is detected (and animations are enabled), $animate will do the heavy lifting + * to ensure that animation runs with the triggered DOM operation. * - * $animate is available in the AngularJS core, however, the ngAnimate module must be included - * to enable full out animation support. Otherwise, $animate will only perform simple DOM - * manipulation operations. + * 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. * - * To learn more about enabling animation support, click here to visit the {@link ngAnimate - * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service - * page}. + * 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 { - animate: function(element, from, to) { - applyStyles(element, { from: from, to: to }); - return asyncPromise(); + // we don't call it directly since non-existant arguments may + // be interpreted as null within the sub enabled function + + /** + * + * @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 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 `listenerFn` that is set + * // to listen for `enter` on the given `element` as well as its children + * $animate.off('enter', container, callback); + * ``` + * + * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...) + * @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 `<body>` 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) { + runner.end && runner.end(); }, /** @@ -4689,206 +5261,448 @@ var $AnimateProvider = ['$provide', function($provide) { * @ngdoc method * @name $animate#enter * @kind function - * @description Inserts the element into the DOM either after the `after` element or - * as the first child within the `parent` element. When the function is called a promise - * is returned that will be resolved at a later time. + * @description Inserts the element into the DOM either after the `after` element (if provided) or + * as the first child within the `parent` element and then triggers an animation. + * A promise is returned that will be resolved during the next digest once the animation + * has completed. + * * @param {DOMElement} element the element which will be inserted into the DOM * @param {DOMElement} parent the parent element which will append the element as - * a child (if the after element is not present) - * @param {DOMElement} after the sibling element which will append the element - * after itself - * @param {object=} options an optional collection of styles that will be applied to the element. + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element + * * @return {Promise} the animation callback promise */ enter: function(element, parent, after, options) { - applyStyles(element, options); - after ? after.after(element) - : parent.prepend(element); - return asyncPromise(); + parent = parent && 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#leave + * @name $animate#move * @kind function - * @description Removes the element from the DOM. When the function is called a promise - * is returned that will be resolved at a later time. - * @param {DOMElement} element the element which will be removed from the DOM - * @param {object=} options an optional collection of options that will be applied to the element. + * @description Inserts (moves) the element into its new position in the DOM either after + * the `after` element (if provided) or as the first child within the `parent` element + * and then triggers an animation. A promise is returned that will be resolved + * during the next digest once the animation has completed. + * + * @param {DOMElement} element the element which will be moved into the new DOM position + * @param {DOMElement} parent the parent element which will append the element as + * a child (so long as the after element is not present) + * @param {DOMElement=} after the sibling element after which the element will be appended + * @param {object=} options an optional collection of options/styles that will be applied to the element + * * @return {Promise} the animation callback promise */ - leave: function(element, options) { - applyStyles(element, options); - element.remove(); - return asyncPromise(); + move: function(element, parent, after, options) { + parent = parent && 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#move + * @name $animate#leave * @kind function - * @description Moves the position of the provided element within the DOM to be placed - * either after the `after` element or inside of the `parent` element. When the function - * is called a promise is returned that will be resolved at a later time. + * @description Triggers an animation and then removes the element from the DOM. + * When the function is called a promise is returned that will be resolved during the next + * digest once the animation has completed. + * + * @param {DOMElement} element the element which will be removed from the DOM + * @param {object=} options an optional collection of options/styles that will be applied to the element * - * @param {DOMElement} element the element which will be moved around within the - * DOM - * @param {DOMElement} parent the parent element where the element will be - * inserted into (if the after element is not present) - * @param {DOMElement} after the sibling element where the element will be - * positioned next to - * @param {object=} options an optional collection of options that will be applied to the element. * @return {Promise} the animation callback promise */ - move: function(element, parent, after, options) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - return this.enter(element, parent, after, options); + leave: function(element, options) { + return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() { + element.remove(); + }); }, /** - * * @ngdoc method * @name $animate#addClass * @kind function - * @description Adds the provided className CSS class value to the provided element. - * When the function is called a promise is returned that will be resolved at a later time. - * @param {DOMElement} element the element which will have the className value - * added to it - * @param {string} className the CSS class which will be added to the element - * @param {object=} options an optional collection of options that will be applied to the element. + * + * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon + * execution, the addClass operation will only be handled after the next digest and it will not trigger an + * animation if element already contains the CSS class or if the class is removed at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element + * * @return {Promise} the animation callback promise */ addClass: function(element, className, options) { - return this.setClass(element, className, [], options); - }, - - $$addClassImmediately: function(element, className, options) { - element = jqLite(element); - className = !isString(className) - ? (isArray(className) ? className.join(' ') : '') - : className; - forEach(element, function(element) { - jqLiteAddClass(element, className); - }); - applyStyles(element, options); - return asyncPromise(); + options = prepareAnimateOptions(options); + options.addClass = mergeClasses(options.addclass, className); + return $$animateQueue.push(element, 'addClass', options); }, /** - * * @ngdoc method * @name $animate#removeClass * @kind function - * @description Removes the provided className CSS class value from the provided element. - * When the function is called a promise is returned that will be resolved at a later time. - * @param {DOMElement} element the element which will have the className value - * removed from it - * @param {string} className the CSS class which will be removed from the element - * @param {object=} options an optional collection of options that will be applied to the element. + * + * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon + * execution, the removeClass operation will only be handled after the next digest and it will not trigger an + * animation if element does not contain the CSS class or if the class is added at a later step. + * Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element + * * @return {Promise} the animation callback promise */ removeClass: function(element, className, options) { - return this.setClass(element, [], className, options); - }, - - $$removeClassImmediately: function(element, className, options) { - element = jqLite(element); - className = !isString(className) - ? (isArray(className) ? className.join(' ') : '') - : className; - forEach(element, function(element) { - jqLiteRemoveClass(element, className); - }); - applyStyles(element, options); - return asyncPromise(); + options = prepareAnimateOptions(options); + options.removeClass = mergeClasses(options.removeClass, className); + return $$animateQueue.push(element, 'removeClass', options); }, /** - * * @ngdoc method * @name $animate#setClass * @kind function - * @description Adds and/or removes the given CSS classes to and from the element. - * When the function is called a promise is returned that will be resolved at a later time. - * @param {DOMElement} element the element which will have its CSS classes changed - * removed from it - * @param {string} add the CSS classes which will be added to the element - * @param {string} remove the CSS class which will be removed from the element - * @param {object=} options an optional collection of options that will be applied to the element. + * + * @description Performs both the addition and removal of a CSS classes on an element and (during the process) + * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and + * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has + * passed. Note that class-based animations are treated differently compared to structural animations + * (like enter, move and leave) since the CSS classes may be added/removed at different points + * depending if CSS or JavaScript animations are used. + * + * @param {DOMElement} element the element which the CSS classes will be applied to + * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) + * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) + * @param {object=} options an optional collection of options/styles that will be applied to the element + * * @return {Promise} the animation callback promise */ setClass: function(element, add, remove, options) { - var self = this; - var STORAGE_KEY = '$$animateClasses'; - var createdCache = false; - element = jqLite(element); - - var cache = element.data(STORAGE_KEY); - if (!cache) { - cache = { - classes: {}, - options: options - }; - createdCache = true; - } else if (options && cache.options) { - cache.options = angular.extend(cache.options || {}, options); - } - - var classes = cache.classes; - - add = isArray(add) ? add : add.split(' '); - remove = isArray(remove) ? remove : remove.split(' '); - cachedClassManipulation(classes, add, true); - cachedClassManipulation(classes, remove, false); - - if (createdCache) { - cache.promise = runAnimationPostDigest(function(done) { - var cache = element.data(STORAGE_KEY); - element.removeData(STORAGE_KEY); - - // in the event that the element is removed before postDigest - // is run then the cache will be undefined and there will be - // no need anymore to add or remove and of the element classes - if (cache) { - var classes = resolveElementClasses(element, cache.classes); - if (classes) { - self.$$setClassImmediately(element, classes[0], classes[1], cache.options); - } - } + 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 className, 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 appplied to the element.) + * @param {object=} options an optional collection of options/styles that will be applied to the element + * + * @return {Promise} the animation callback promise + */ + animate: function(element, from, to, className, options) { + options = 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); + } + }; + }]; +}]; + +var $$AnimateAsyncRunFactoryProvider = function() { + this.$get = ['$$rAF', function($$rAF) { + var waitQueue = []; + + function waitForTick(fn) { + waitQueue.push(fn); + if (waitQueue.length > 1) return; + $$rAF(function() { + for (var i = 0; i < waitQueue.length; i++) { + waitQueue[i](); + } + waitQueue = []; + }); + } + + return function() { + var passed = false; + waitForTick(function() { + passed = true; + }); + return function(callback) { + passed ? callback() : waitForTick(callback); + }; + }; + }]; +}; + +var $$AnimateRunnerFactoryProvider = function() { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $document, $timeout) { + + var INITIAL_STATE = 0; + var DONE_PENDING_STATE = 1; + var DONE_COMPLETE_STATE = 2; + + AnimateRunner.chain = function(chain, callback) { + var index = 0; + + next(); + function next() { + if (index === chain.length) { + callback(true); + return; + } + + chain[index](function(response) { + if (response === false) { + callback(false); + return; + } + index++; + next(); + }); + } + }; + + AnimateRunner.all = function(runners, callback) { + var count = 0; + var status = true; + forEach(runners, function(runner) { + runner.done(onProgress); + }); + + function onProgress(response) { + status = status && response; + if (++count === runners.length) { + callback(status); + } + } + }; + + function AnimateRunner(host) { + this.setHost(host); + + var rafTick = $$animateAsyncRun(); + var timeoutTick = function(fn) { + $timeout(fn, 0, false); + }; + + this._doneCallbacks = []; + this._tick = function(fn) { + var doc = $document[0]; + + // the document may not be ready or attached + // to the module for some internal tests + if (doc && doc.hidden) { + timeoutTick(fn); + } else { + rafTick(fn); + } + }; + this._state = 0; + } + + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, + + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, + + progress: noop, - done(); + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + status === false ? reject() : resolve(); + }); }); - element.data(STORAGE_KEY, cache); } + return this.promise; + }, - return cache.promise; + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); }, - $$setClassImmediately: function(element, add, remove, options) { - add && this.$$addClassImmediately(element, add); - remove && this.$$removeClassImmediately(element, remove); - applyStyles(element, options); - return asyncPromise(); + 'catch': function(handler) { + return this.getPromise()['catch'](handler); }, - enabled: noop, - cancel: noop + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); + } + }, + + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, + + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, + + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, + + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._tick(function() { + self._resolve(response); + }); + } + }, + + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; + } + } }; + + return AnimateRunner; }]; -}]; +}; -function $$AsyncCallbackProvider() { - this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { - return $$rAF.supported - ? function(fn) { return $$rAF(fn); } - : function(fn) { - return $timeout(fn, 0, false); +/** + * @ngdoc service + * @name $animateCss + * @kind object + * + * @description + * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, + * then the `$animateCss` service will actually perform animations. + * + * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. + */ +var $CoreAnimateCssProvider = function() { + this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { + + return function(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 = copy(options); + } + + // there is no point in applying the styles since + // there is no animation that goes on at all in + // this version of $animateCss. + if (options.cleanupStyles) { + options.from = options.to = null; + } + + if (options.from) { + element.css(options.from); + options.from = null; + } + + /* jshint newcap: false*/ + var closed, runner = new $$AnimateRunner(); + return { + start: run, + end: run }; + + function run() { + $$rAF(function() { + applyAnimationContents(); + if (!closed) { + runner.complete(); + } + closed = true; + }); + return runner; + } + + function applyAnimationContents() { + if (options.addClass) { + element.addClass(options.addClass); + options.addClass = null; + } + if (options.removeClass) { + element.removeClass(options.removeClass); + options.removeClass = null; + } + if (options.to) { + element.css(options.to); + options.to = null; + } + } + }; }]; -} +}; /* global stripHash: true */ @@ -4964,11 +5778,6 @@ function Browser(window, document, $log, $sniffer) { * @param {function()} callback Function that will be called when no outstanding request */ self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn) { pollFn(); }); - if (outstandingRequestCount === 0) { callback(); } else { @@ -4977,51 +5786,13 @@ function Browser(window, document, $log, $sniffer) { }; ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @name $browser#addPollFn - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn) { pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - - ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// var cachedState, lastHistoryState, lastBrowserUrl = location.href, baseElement = document.find('base'), - reloadLocation = null; + pendingLocation = null; cacheState(); lastHistoryState = cachedState; @@ -5081,8 +5852,8 @@ function Browser(window, document, $log, $sniffer) { // Do the assignment again so that those two variables are referentially identical. lastHistoryState = cachedState; } else { - if (!sameBase || reloadLocation) { - reloadLocation = url; + if (!sameBase || pendingLocation) { + pendingLocation = url; } if (replace) { location.replace(url); @@ -5091,14 +5862,18 @@ function Browser(window, document, $log, $sniffer) { } else { location.hash = getHash(url); } + if (location.href !== url) { + pendingLocation = url; + } } return self; // getter } else { - // - reloadLocation is needed as browsers don't allow to read out - // the new location.href if a reload happened. + // - pendingLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened or if there is a bug like in iOS 9 (see + // https://openradar.appspot.com/22186109). // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return reloadLocation || location.href.replace(/%27/g,"'"); + return pendingLocation || location.href.replace(/%27/g,"'"); } }; @@ -5120,6 +5895,7 @@ function Browser(window, document, $log, $sniffer) { urlChangeInit = false; function cacheStateAndFireUrlChange() { + pendingLocation = null; cacheState(); fireUrlChange(); } @@ -5199,6 +5975,16 @@ function Browser(window, document, $log, $sniffer) { }; /** + * @private + * Remove popstate and hashchange handler from window. + * + * NOTE: this api is intended for use only by $rootScope. + */ + self.$$applicationDestroyed = function() { + jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); + }; + + /** * Checks whether the url has changed outside of Angular. * Needs to be exported to be able to check for changes that have been done in sync, * as hashchange/popstate events fire in async. @@ -5223,89 +6009,6 @@ function Browser(window, document, $log, $sniffer) { return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; }; - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - var cookiePath = self.baseHref(); - - function safeDecodeURIComponent(str) { - try { - return decodeURIComponent(str); - } catch (e) { - return str; - } - } - - /** - * @name $browser#cookies - * - * @param {string=} name Cookie name - * @param {string=} value Cookie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify - * it - * - cookies(name, value) -> set name to value, if value is undefined delete the cookie - * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that - * way) - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - var cookieLength, cookieArray, cookie, i, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + - ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + - ';path=' + cookiePath).length + 1; - - // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: - // - 300 cookies - // - 20 cookies per unique domain - // - 4096 bytes per cookie - if (cookieLength > 4096) { - $log.warn("Cookie '" + name + - "' possibly not set or overflowed because it was too large (" + - cookieLength + " > 4096 bytes)!"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - name = safeDecodeURIComponent(cookie.substring(0, index)); - // the first value that is seen for a cookie is the most - // specific one. values for the same cookie name that - // follow are for less specific paths. - if (lastCookies[name] === undefined) { - lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); - } - } - } - } - return lastCookies; - } - }; - - /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. @@ -5428,10 +6131,10 @@ function $BrowserProvider() { $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - if ($scope.cache.get(key) === undefined) { + if (angular.isUndefined($scope.cache.get(key))) { $scope.keys.push(key); } - $scope.cache.put(key, value === undefined ? null : value); + $scope.cache.put(key, angular.isUndefined(value) ? null : value); }; }]); </file> @@ -5454,9 +6157,9 @@ function $CacheFactoryProvider() { var size = 0, stats = extend({}, options, {id: cacheId}), - data = {}, + data = createMap(), capacity = (options && options.capacity) || Number.MAX_VALUE, - lruHash = {}, + lruHash = createMap(), freshEnd = null, staleEnd = null; @@ -5520,13 +6223,13 @@ function $CacheFactoryProvider() { * @returns {*} the value stored. */ put: function(key, value) { + if (isUndefined(value)) return; if (capacity < Number.MAX_VALUE) { var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); refresh(lruEntry); } - if (isUndefined(value)) return; if (!(key in data)) size++; data[key] = value; @@ -5584,6 +6287,8 @@ function $CacheFactoryProvider() { delete lruHash[key]; } + if (!(key in data)) return; + delete data[key]; size--; }, @@ -5598,9 +6303,9 @@ function $CacheFactoryProvider() { * Clears the cache object of any entries. */ removeAll: function() { - data = {}; + data = createMap(); size = 0; - lruHash = {}; + lruHash = createMap(); freshEnd = staleEnd = null; }, @@ -5907,59 +6612,95 @@ function $TemplateCacheProvider() { * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the - * same element request a new scope, only one new scope is created. The new scope rule does not - * apply for the root of the template since the root of the template always gets a new scope. + * The scope property can be `true`, an object or a falsy value: + * + * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. * - * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from - * normal scope in that it does not prototypically inherit from the parent scope. This is useful - * when creating reusable components, which should not accidentally read or modify data in the - * parent scope. + * * **`true`:** A new child scope that prototypically inherits from its parent will be created for + * the directive's element. If multiple directives on the same element request a new scope, + * only one new scope is created. The new scope rule does not apply for the root of the template + * since the root of the template always gets a new scope. * - * The 'isolate' scope takes an object hash which defines a set of local scope properties - * derived from the parent scope. These local properties are useful for aliasing values for - * templates. Locals definition is a hash of local scope property to its source: + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The + * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent + * scope. This is useful when creating reusable components, which should not accidentally read or modify + * data in the parent scope. + * + * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the + * directive's element. These local properties are useful for aliasing values for templates. The keys in + * the object hash map to the name of the property on the isolate scope; the values define how the property + * is bound to the parent scope, via matching attributes on the directive's element: * * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `<widget my-attr="hello {{name}}">` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). - * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `<widget my-attr="parentModel">` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the - * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected - * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent - * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You - * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If - * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use - * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). - * - * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. - * If no `attr` name is specified then the attribute name is assumed to be the same as the - * local name. Given `<widget my-attr="count = count + value">` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. Given `<my-component + * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`, + * the directive's scope property `localName` will reflect the interpolated value of `hello + * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's + * scope. The `name` is read from the parent scope (not the directive's scope). + * + * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression + * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the local + * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: { + * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the + * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in + * `localModel` and vice versa. Optional attributes should be marked as such with a question mark: + * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't + * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`}) + * will be thrown upon discovering changes to the local value, since it will be impossible to sync + * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`} + * method is used for tracking changes, and the equality check is based on object identity. + * However, if an object literal or an array literal is passed as the binding expression, the + * equality check is done by value (using the {@link angular.equals} function). It's also possible + * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection + * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional). + * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If + * no `attr` name is specified then the attribute name is assumed to be the same as the local name. + * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: { + * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for + * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope + * via an expression to the parent scope. This can be done by passing a map of local variable names + * and values into the expression wrapper fn. For example, if the expression is `increment(amount)` + * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`. + * + * In general it's possible to apply more than one directive to one element, but there might be limitations + * depending on the type of scope required by the directives. The following points will help explain these limitations. + * For simplicity only two directives are taken into account, but it is also applicable for several directives: + * + * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope + * * **child scope** + **no scope** => Both directives will share one single child scope + * * **child scope** + **child scope** => Both directives will share one single child scope + * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use + * its parent's scope + * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot + * be applied to the same element. + * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives + * cannot be applied to the same element. * * * #### `bindToController` - * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will + * This property is used to bind scope properties directly to the controller. It can be either + * `true` or an object hash with the same format as the `scope` property. Additionally, a controller + * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller + * definition: `controller: 'myCtrl as myAlias'`. + * + * When an isolate scope is used for a directive (see above), `bindToController: true` will * allow a component to have its properties bound to the controller, rather than to scope. When the controller * is instantiated, the initial values of the isolate scope bindings are already available. * + * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. + * This will set up the scope bindings to the controller directly. Note that `scope` can still be used + * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate + * scope (useful for component directives). + * + * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`. + * + * * #### `controller` * Controller constructor function. The controller is instantiated before the - * pre-linking phase and it is shared with other directives (see + * pre-linking phase and can be accessed by other directives (see * `require` attribute). This allows the directives to communicate with each other and augment * each other's behavior. The controller is injectable (and supports bracket notation) with the following locals: * @@ -5998,9 +6739,11 @@ function $TemplateCacheProvider() { * * * #### `controllerAs` - * Controller alias at the directive scope. An alias for the controller so it - * can be referenced at the directive template. The directive needs to define a scope for this - * configuration to be used. Useful in the case when directive is used as component. + * Identifier name for a reference to the controller in the directive's scope. + * This allows the controller to be referenced from the directive template. This is especially + * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible + * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the + * `controllerAs` reference might overwrite a property that already exists on the parent scope. * * * #### `restrict` @@ -6111,7 +6854,7 @@ function $TemplateCacheProvider() { * <div class="alert alert-warning"> * **Note:** The compile function cannot handle directives that recursively use themselves in their - * own templates or compile functions. Compiling these directives results in an infinite loop and a + * own templates or compile functions. Compiling these directives results in an infinite loop and * stack overflow errors. * * This can be avoided by manually using $compile in the postLink function to imperatively compile @@ -6119,7 +6862,7 @@ function $TemplateCacheProvider() { * `templateUrl` declaration or manual compilation inside the compile function. * </div> * - * <div class="alert alert-error"> + * <div class="alert alert-danger"> * **Note:** The `transclude` function that is passed to the compile function is deprecated, as it * e.g. does not know about the right outer scope. Please use the transclude function that is passed * to the link function instead. @@ -6159,13 +6902,16 @@ function $TemplateCacheProvider() { * * `controller` - the directive's required controller instance(s) - Instances are shared * among all directives, which allows the directives to use the controllers as a communication * channel. The exact value depends on the directive's `require` property: + * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one * * `string`: the controller instance * * `array`: array of controller instances - * * no controller(s) required: `undefined` * * If a required controller cannot be found, and it is optional, the instance is `null`, * otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown. * + * Note that you can also require the directive's own controller - it will be made available like + * any other controller. + * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. * This is the same as the `$transclude` * parameter of directive controllers, see there for details. @@ -6257,7 +7003,7 @@ function $TemplateCacheProvider() { * * <div class="alert alert-info"> * **Best Practice**: if you intend to add and remove transcluded content manually in your directive - * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it), + * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it), * then you are also responsible for calling `$destroy` on the transclusion scope. * </div> * @@ -6287,19 +7033,19 @@ function $TemplateCacheProvider() { * * The `$parent` scope hierarchy will look like this: * - * ``` - * - $rootScope - * - isolate - * - transclusion - * ``` + ``` + - $rootScope + - isolate + - transclusion + ``` * * but the scopes will inherit prototypically from different scopes to their `$parent`. * - * ``` - * - $rootScope - * - transclusion - * - isolate - * ``` + ``` + - $rootScope + - transclusion + - isolate + ``` * * * ### Attributes @@ -6307,10 +7053,9 @@ function $TemplateCacheProvider() { * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the * `link()` or `compile()` functions. It has a variety of uses. * - * accessing *Normalized attribute names:* - * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. - * the attributes object allows for normalized access to - * the attributes. + * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways: + * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access + * to the attributes. * * * *Directive inter-communication:* All directives share the same instance of the attributes * object which allows the directives to use the attributes object as inter directive @@ -6381,8 +7126,8 @@ function $TemplateCacheProvider() { }]); </script> <div ng-controller="GreeterController"> - <input ng-model="name"> <br> - <textarea ng-model="html"></textarea> <br> + <input ng-model="name"> <br/> + <textarea ng-model="html"></textarea> <br/> <div compile="html"></div> </div> </file> @@ -6404,7 +7149,7 @@ function $TemplateCacheProvider() { * @param {string|DOMElement} element Element or HTML string to compile into a template function. * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. * - * <div class="alert alert-error"> + * <div class="alert alert-danger"> * **Note:** Passing a `transclude` function to the $compile function is deprecated, as it * e.g. will not use the right outer scope. Please pass the transclude function as a * `parentBoundTranscludeFn` to the link function instead. @@ -6419,7 +7164,7 @@ function $TemplateCacheProvider() { * * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the * `template` and call the `cloneAttachFn` function allowing the caller to attach the * cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is - * called as: <br> `cloneAttachFn(clonedElement, scope)` where: + * called as: <br/> `cloneAttachFn(clonedElement, scope)` where: * * * `clonedElement` - is a clone of the original `element` passed into the compiler. * * `scope` - is the current scope with which the linking function is working with. @@ -6431,8 +7176,15 @@ function $TemplateCacheProvider() { * directives; if given, it will be passed through to the link functions of * directives found in `element` during compilation. * * `transcludeControllers` - an object hash with keys that map controller names - * to controller instances; if given, it will make the controllers - * available to directives. + * to a hash with the key `instance`, which maps to the controller instance; + * if given, it will make the controllers available to directives on the compileNode: + * ``` + * { + * parent: { + * instance: parentControllerInstance + * } + * } + * ``` * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add * the cloned elements; only needed for transcludes that are allowed to contain non html * elements (e.g. SVG elements). See also the directive.controller property. @@ -6491,20 +7243,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var bindingCache = createMap(); - function parseIsolateBindings(scope, directiveName) { + function parseIsolateBindings(scope, directiveName, isController) { var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; var bindings = {}; forEach(scope, function(definition, scopeName) { + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } var match = definition.match(LOCAL_REGEXP); if (!match) { throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + + "Invalid {3} for directive '{0}'." + " Definition: {... {1}: '{2}' ...}", - directiveName, scopeName, definition); + directiveName, scopeName, definition, + (isController ? "controller bindings definition" : + "isolate scope definition")); } bindings[scopeName] = { @@ -6513,17 +7272,61 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { optional: match[3] === '?', attrName: match[4] || scopeName }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } }); return bindings; } + function parseDirectiveBindings(directive, directiveName) { + var bindings = { + isolateScope: null, + bindToController: null + }; + if (isObject(directive.scope)) { + if (directive.bindToController === true) { + bindings.bindToController = parseIsolateBindings(directive.scope, + directiveName, true); + bindings.isolateScope = {}; + } else { + bindings.isolateScope = parseIsolateBindings(directive.scope, + directiveName, false); + } + } + if (isObject(directive.bindToController)) { + bindings.bindToController = + parseIsolateBindings(directive.bindToController, directiveName, true); + } + if (isObject(bindings.bindToController)) { + var controller = directive.controller; + var controllerAs = directive.controllerAs; + if (!controller) { + // There is no controller, there may or may not be a controllerAs property + throw $compileMinErr('noctrl', + "Cannot bind to controller without directive '{0}'s controller.", + directiveName); + } else if (!identifierForController(controller, controllerAs)) { + // There is a controller, but no identifier or controllerAs property + throw $compileMinErr('noident', + "Cannot bind to controller without identifier for directive '{0}'.", + directiveName); + } + } + return bindings; + } + function assertValidDirectiveName(name) { var letter = name.charAt(0); if (!letter || letter !== lowercase(letter)) { throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name); } - return name; + if (name !== name.trim()) { + throw $compileMinErr('baddir', + "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", + name); + } } /** @@ -6564,9 +7367,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); directive.restrict = directive.restrict || 'EA'; - if (isObject(directive.scope)) { - directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name); - } + directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -6675,9 +7476,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', + '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { + $controller, $rootScope, $sce, $animate, $$sanitizeUri) { var Attributes = function(element, attributesToCopy) { if (attributesToCopy) { @@ -6788,7 +7589,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), - aliasedKey = getAliasedAttrName(node, key), + aliasedKey = getAliasedAttrName(key), observer = key, nodeName; @@ -6855,7 +7656,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (writeAttr !== false) { - if (value === null || value === undefined) { + if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { this.$$element.attr(attrName, value); @@ -6889,7 +7690,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. + * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation + * guide} for more info. * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { @@ -6899,7 +7701,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter && attrs.hasOwnProperty(key)) { + if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } @@ -6924,12 +7726,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') + denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { var bindings = $element.data('$binding') || []; @@ -6967,13 +7770,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // modify it. $compileNodes = jqLite($compileNodes); } + + var NOT_EMPTY = /\S+/; + // We can not compile top level text elements since text nodes can be merged and we will // not be able to attach scope data to them, so we will wrap them in <span> - forEach($compileNodes, function(node, index) { - if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0]; + for (var i = 0, len = $compileNodes.length; i < len; i++) { + var domNode = $compileNodes[i]; + + if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) { + jqLiteWrapNode(domNode, $compileNodes[i] = document.createElement('span')); } - }); + } + var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); @@ -6982,6 +7791,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return function publicLinkFn(scope, cloneConnectFn, options) { assertArg(scope, 'scope'); + if (previousCompileContext && previousCompileContext.needsNewScope) { + // A parent directive did a replace and a directive on this element asked + // for transclusion, which caused us to lose a layer of element on which + // we could hold the new transclusion scope, so we will create it manually + // here. + scope = scope.$parent.$new(); + } + options = options || {}; var parentBoundTranscludeFn = options.parentBoundTranscludeFn, transcludeControllers = options.transcludeControllers, @@ -7133,8 +7950,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (nodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn( - scope, nodeLinkFn.transclude, parentBoundTranscludeFn, - nodeLinkFn.elementTranscludeOnThisElement); + scope, nodeLinkFn.transclude, parentBoundTranscludeFn); } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; @@ -7155,7 +7971,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { @@ -7215,13 +8031,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); } - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); - if (directiveIsMultiElement(directiveNName)) { - if (ngAttrName === directiveNName + 'Start') { - attrStartName = name; - attrEndName = name.substr(0, name.length - 5) + 'end'; - name = name.substr(0, name.length - 6); - } + var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE); + if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) { + attrStartName = name; + attrEndName = name.substr(0, name.length - 5) + 'end'; + name = name.substr(0, name.length - 6); } nName = directiveNormalize(name.toLowerCase()); @@ -7254,6 +8068,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } break; case NODE_TYPE_TEXT: /* Text Node */ + if (msie === 11) { + // Workaround for #11781 + while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { + node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; + node.parentNode.removeChild(node.nextSibling); + } + } addTextInterpolateDirective(directives, node.nodeValue); break; case NODE_TYPE_COMMENT: /* Comment */ @@ -7353,9 +8174,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { previousCompileContext = previousCompileContext || {}; var terminalPriority = -Number.MAX_VALUE, - newScopeDirective, + newScopeDirective = previousCompileContext.newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, - controllers, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, @@ -7413,7 +8233,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!directive.templateUrl && directive.controller) { directiveValue = directive.controller; - controllerDirectives = controllerDirectives || {}; + controllerDirectives = controllerDirectives || createMap(); assertNoDuplicate("'" + directiveName + "' controller", controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; @@ -7454,7 +8274,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else { $template = jqLite(jqLiteClone(compileNode)).contents(); $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn); + childTranscludeFn = compile($template, transcludeFn, undefined, + undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope}); } } @@ -7496,8 +8317,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs); var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1)); - if (newIsolateScopeDirective) { - markDirectivesAsIsolate(templateDirectives); + if (newIsolateScopeDirective || newScopeDirective) { + // The original directive caused the current element to be replaced but this element + // also needs to have a new scope, so we need to tell the template directives + // that they would need to get their scope from further up, if they require transclusion + markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective); } directives = directives.concat(templateDirectives).concat(unprocessedDirectives); mergeTemplateAttributes(templateAttrs, newTemplateAttrs); @@ -7520,6 +8344,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, + newScopeDirective: (newScopeDirective !== directive) && newScopeDirective, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, nonTlbTranscludeDirective: nonTlbTranscludeDirective @@ -7547,7 +8372,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; - nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; nodeLinkFn.templateOnThisElement = hasTemplate; nodeLinkFn.transclude = childTranscludeFn; @@ -7581,55 +8405,75 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function getControllers(directiveName, require, $element, elementControllers) { - var value, retrievalMethod = 'data', optional = false; - var $searchElement = $element; - var match; - if (isString(require)) { - match = require.match(REQUIRE_PREFIX_REGEXP); - require = require.substring(match[0].length); + var value; - if (match[3]) { - if (match[1]) match[3] = null; - else match[1] = match[3]; - } - if (match[1] === '^') { - retrievalMethod = 'inheritedData'; - } else if (match[1] === '^^') { - retrievalMethod = 'inheritedData'; - $searchElement = $element.parent(); - } - if (match[2] === '?') { - optional = true; + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; } - value = null; - - if (elementControllers && retrievalMethod === 'data') { - if (value = elementControllers[require]) { - value = value.instance; - } + if (!value) { + var dataName = '$' + name + 'Controller'; + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); } - value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", - require, directiveName); + name, directiveName); } - return value || null; } else if (isArray(require)) { value = []; - forEach(require, function(require) { - value.push(getControllers(directiveName, require, $element, elementControllers)); - }); + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } } - return value; + + return value || null; } + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller == '@') { + controller = attrs[directive.name]; + } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + return elementControllers; + } function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, - attrs; + var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, removeScopeBindingWatches, removeControllerBindingWatches; if (compileNode === linkNode) { attrs = templateAttrs; @@ -7639,8 +8483,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrs = new Attributes($element, templateAttrs); } + controllerScope = scope; if (newIsolateScopeDirective) { isolateScope = scope.$new(true); + } else if (newScopeDirective) { + controllerScope = scope.$parent; } if (boundTranscludeFn) { @@ -7651,126 +8498,45 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (controllerDirectives) { - // TODO: merge `controllers` and `elementControllers` into single object. - controllers = {}; - elementControllers = {}; - forEach(controllerDirectives, function(directive) { - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }, controllerInstance; - - controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - controllerInstance = $controller(controller, locals, true, directive.controllerAs); - - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance.instance); - } - - controllers[directive.name] = controllerInstance; - }); + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); } if (newIsolateScopeDirective) { + // Initialize isolate scope bindings for new isolate scope directive. compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective || templateDirective === newIsolateScopeDirective.$$originalDirective))); compile.$$addScopeClass($element, true); - - var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name]; - var isolateBindingContext = isolateScope; - if (isolateScopeController && isolateScopeController.identifier && - newIsolateScopeDirective.bindToController === true) { - isolateBindingContext = isolateScopeController.instance; + isolateScope.$$isolateBindings = + newIsolateScopeDirective.$$isolateBindings; + removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope, + isolateScope.$$isolateBindings, + newIsolateScopeDirective); + if (removeScopeBindingWatches) { + isolateScope.$on('$destroy', removeScopeBindingWatches); } + } - forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) { - var attrName = definition.attrName, - optional = definition.optional, - mode = definition.mode, // @, =, or & - lastValue, - parentGet, parentSet, compare; - - switch (mode) { - - case '@': - attrs.$observe(attrName, function(value) { - isolateBindingContext[scopeName] = value; - }); - attrs.$$observers[attrName].$$scope = scope; - if (attrs[attrName]) { - // If the attribute has been provided then we trigger an interpolation to ensure - // the value is there for use in the link fn - isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); - } - break; + // Initialize bindToController bindings + for (var name in elementControllers) { + var controllerDirective = controllerDirectives[name]; + var controller = elementControllers[name]; + var bindings = controllerDirective.$$bindings.bindToController; - case '=': - if (optional && !attrs[attrName]) { - return; - } - parentGet = $parse(attrs[attrName]); - if (parentGet.literal) { - compare = equals; - } else { - compare = function(a, b) { return a === b || (a !== a && b !== b); }; - } - parentSet = parentGet.assign || function() { - // reset the change, or we will throw this exception on every $digest - lastValue = isolateBindingContext[scopeName] = parentGet(scope); - throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], newIsolateScopeDirective.name); - }; - lastValue = isolateBindingContext[scopeName] = parentGet(scope); - var parentValueWatch = function parentValueWatch(parentValue) { - if (!compare(parentValue, isolateBindingContext[scopeName])) { - // we are out of sync and need to copy - if (!compare(parentValue, lastValue)) { - // parent changed and it has precedence - isolateBindingContext[scopeName] = parentValue; - } else { - // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateBindingContext[scopeName]); - } - } - return lastValue = parentValue; - }; - parentValueWatch.$stateful = true; - var unwatch; - if (definition.collection) { - unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch); - } else { - unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); - } - isolateScope.$on('$destroy', unwatch); - break; + if (controller.identifier && bindings) { + removeControllerBindingWatches = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } - case '&': - parentGet = $parse(attrs[attrName]); - isolateBindingContext[scopeName] = function(locals) { - return parentGet(scope, locals); - }; - break; - } - }); - } - if (controllers) { - forEach(controllers, function(controller) { - controller(); - }); - controllers = null; + var controllerResult = controller(); + if (controllerResult !== controller.instance) { + // If the controller constructor has a return value, overwrite the instance + // from setupControllers + controller.instance = controllerResult; + $element.data('$' + controllerDirective.name + 'Controller', controllerResult); + removeControllerBindingWatches && removeControllerBindingWatches(); + removeControllerBindingWatches = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } } // PRELINKING @@ -7829,10 +8595,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function markDirectivesAsIsolate(directives) { - // mark all directives as needing isolate scope. + // Depending upon the context in which a directive finds itself it might need to have a new isolated + // or child scope created. For instance: + // * if the directive has been pulled into a template because another directive with a higher priority + // asked for element transclusion + // * if the directive itself asks for transclusion but it is at the root of a template and the original + // element was replaced. See https://github.com/angular/angular.js/issues/12936 + function markDirectiveScope(directives, isolateScope, newScope) { for (var j = 0, jj = directives.length; j < jj; j++) { - directives[j] = inherit(directives[j], {$$isolateScope: true}); + directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope}); } } @@ -7859,11 +8630,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { i = 0, ii = directives.length; i < ii; i++) { try { directive = directives[i]; - if ((maxPriority === undefined || maxPriority > directive.priority) && + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && directive.restrict.indexOf(location) != -1) { if (startAttrName) { directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; + } + } tDirectives.push(directive); match = directive; } @@ -7979,7 +8757,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs); if (isObject(origAsyncDirective.scope)) { - markDirectivesAsIsolate(templateDirectives); + // the original directive that caused the template to be loaded async required + // an isolate scope + markDirectiveScope(templateDirectives, true); } directives = templateDirectives.concat(directives); mergeTemplateAttributes(tAttrs, tempTemplateAttrs); @@ -8061,11 +8841,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return a.index - b.index; } - function assertNoDuplicate(what, previousDirective, directive, element) { + + function wrapModuleNameIfDefined(moduleName) { + return moduleName ? + (' (module: ' + moduleName + ')') : + ''; + } + if (previousDirective) { - throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', - previousDirective.name, directive.name, what, startingTag(element)); + throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}', + previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName), + directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element)); } } @@ -8147,7 +8934,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile: function() { return { pre: function attrInterpolatePreLinkFn(scope, element, attr) { - var $$observers = (attr.$$observers || (attr.$$observers = {})); + var $$observers = (attr.$$observers || (attr.$$observers = createMap())); if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { throw $compileMinErr('nodomevents', @@ -8246,26 +9033,28 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); - // Copy over user data (that includes Angular's $scope etc.). Don't copy private - // data here because there's no public interface in jQuery to do that and copying over - // event listeners (which is the main use of private data) wouldn't work anyway. - jqLite(newNode).data(jqLite(firstElementToRemove).data()); - - // Remove data of the replaced element. We cannot just call .remove() - // on the element it since that would deallocate scope that is needed - // for the new node. Instead, remove the data "manually". - if (!jQuery) { - delete jqLite.cache[firstElementToRemove[jqLite.expando]]; - } else { - // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after - // the replaced element. The cleanData version monkey-patched by Angular would cause - // the scope to be trashed and we do need the very same scope to work with the new - // element. However, we cannot just cache the non-patched version and use it here as - // that would break if another library patches the method after Angular does (one - // example is jQuery UI). Instead, set a flag indicating scope destroying should be - // skipped this one time. - skipDestroyOnNextJQueryCleanData = true; - jQuery.cleanData([firstElementToRemove]); + if (jqLite.hasData(firstElementToRemove)) { + // Copy over user data (that includes Angular's $scope etc.). Don't copy private + // data here because there's no public interface in jQuery to do that and copying over + // event listeners (which is the main use of private data) wouldn't work anyway. + jqLite.data(newNode, jqLite.data(firstElementToRemove)); + + // Remove data of the replaced element. We cannot just call .remove() + // on the element it since that would deallocate scope that is needed + // for the new node. Instead, remove the data "manually". + if (!jQuery) { + delete jqLite.cache[firstElementToRemove[jqLite.expando]]; + } else { + // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after + // the replaced element. The cleanData version monkey-patched by Angular would cause + // the scope to be trashed and we do need the very same scope to work with the new + // element. However, we cannot just cache the non-patched version and use it here as + // that would break if another library patches the method after Angular does (one + // example is jQuery UI). Instead, set a flag indicating scope destroying should be + // skipped this one time. + skipDestroyOnNextJQueryCleanData = true; + jQuery.cleanData([firstElementToRemove]); + } } for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { @@ -8292,6 +9081,107 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { $exceptionHandler(e, startingTag($element)); } } + + + // Set up $watches for isolate scope and controller bindings. This process + // only occurs for isolate scopes and new scopes with controllerAs. + function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { + var removeWatchCollection = []; + forEach(bindings, function(definition, scopeName) { + var attrName = definition.attrName, + optional = definition.optional, + mode = definition.mode, // @, =, or & + lastValue, + parentGet, parentSet, compare; + + switch (mode) { + + case '@': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + destination[scopeName] = attrs[attrName] = void 0; + } + attrs.$observe(attrName, function(value) { + if (isString(value)) { + destination[scopeName] = value; + } + }); + attrs.$$observers[attrName].$$scope = scope; + lastValue = attrs[attrName]; + if (isString(lastValue)) { + // If the attribute has been provided then we trigger an interpolation to ensure + // the value is there for use in the link fn + destination[scopeName] = $interpolate(lastValue)(scope); + } else if (isBoolean(lastValue)) { + // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted + // the value to boolean rather than a string, so we special case this situation + destination[scopeName] = lastValue; + } + break; + + case '=': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + attrs[attrName] = void 0; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + if (parentGet.literal) { + compare = equals; + } else { + compare = function(a, b) { return a === b || (a !== a && b !== b); }; + } + parentSet = parentGet.assign || function() { + // reset the change, or we will throw this exception on every $digest + lastValue = destination[scopeName] = parentGet(scope); + throw $compileMinErr('nonassign', + "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!", + attrs[attrName], attrName, directive.name); + }; + lastValue = destination[scopeName] = parentGet(scope); + var parentValueWatch = function parentValueWatch(parentValue) { + if (!compare(parentValue, destination[scopeName])) { + // we are out of sync and need to copy + if (!compare(parentValue, lastValue)) { + // parent changed and it has precedence + destination[scopeName] = parentValue; + } else { + // if the parent can be assigned then do so + parentSet(scope, parentValue = destination[scopeName]); + } + } + return lastValue = parentValue; + }; + parentValueWatch.$stateful = true; + var removeWatch; + if (definition.collection) { + removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); + } else { + removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal); + } + removeWatchCollection.push(removeWatch); + break; + + case '&': + // Don't assign Object.prototype method to scope + parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; + + // Don't assign noop to destination if expression is not valid + if (parentGet === noop && optional) break; + + destination[scopeName] = function(locals) { + return parentGet(scope, locals); + }; + break; + } + }); + + return removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } + }; + } }]; } @@ -8399,6 +9289,17 @@ function removeComments(jqNodes) { var $controllerMinErr = minErr('$controller'); + +var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; +function identifierForController(controller, ident) { + if (ident && isString(ident)) return ident; + if (isString(controller)) { + var match = CNTRL_REG.exec(controller); + if (match) return match[3]; + } +} + + /** * @ngdoc provider * @name $controllerProvider @@ -8411,9 +9312,7 @@ var $controllerMinErr = minErr('$controller'); */ function $ControllerProvider() { var controllers = {}, - globals = false, - CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; - + globals = false; /** * @ngdoc method @@ -8521,8 +9420,16 @@ function $ControllerProvider() { addIdentifier(locals, identifier, instance, constructor || expression.name); } - return extend(function() { - $injector.invoke(expression, instance, locals, constructor); + var instantiate; + return instantiate = extend(function() { + var result = $injector.invoke(expression, instance, locals, constructor); + if (result !== instance && (isObject(result) || isFunction(result))) { + instance = result; + if (identifier) { + // If result changed, re-assign controllerAs value to scope. + addIdentifier(locals, identifier, instance, constructor || expression.name); + } + } return instance; }, { instance: instance, @@ -8630,6 +9537,29 @@ function $ExceptionHandlerProvider() { }]; } +var $$ForceReflowProvider = function() { + this.$get = ['$document', function($document) { + return function(domNode) { + //the line below will force the browser to perform a repaint so + //that all the animated elements within the animation frame will + //be properly updated and drawn on screen. This is required to + //ensure that the preparation animation is properly flushed so that + //the active state picks up from there. DO NOT REMOVE THIS LINE. + //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH + //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND + //WILL TAKE YEARS AWAY FROM YOUR LIFE. + if (domNode) { + if (!domNode.nodeType && domNode instanceof jqLite) { + domNode = domNode[0]; + } + } else { + domNode = $document[0].body; + } + return domNode.offsetWidth + 1; + }; + }]; +}; + var APPLICATION_JSON = 'application/json'; var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; var JSON_START = /^\[|^\{(?!\{)/; @@ -8638,6 +9568,129 @@ var JSON_ENDS = { '{': /}$/ }; var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +var $httpMinErr = minErr('$http'); +var $httpMinErrLegacyFn = function(method) { + return function() { + throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); + }; +}; + +function serializeValue(v) { + if (isObject(v)) { + return isDate(v) ? v.toISOString() : toJson(v); + } + return v; +} + + +function $HttpParamSerializerProvider() { + /** + * @ngdoc service + * @name $httpParamSerializer + * @description + * + * Default {@link $http `$http`} params serializer that converts objects to strings + * according to the following rules: + * + * * `{'foo': 'bar'}` results in `foo=bar` + * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) + * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) + * + * Note that serializer will sort the request parameters alphabetically. + * */ + + this.$get = function() { + return function ngParamSerializer(params) { + if (!params) return ''; + var parts = []; + forEachSorted(params, function(value, key) { + if (value === null || isUndefined(value)) return; + if (isArray(value)) { + forEach(value, function(v, k) { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); + }); + } else { + parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value))); + } + }); + + return parts.join('&'); + }; + }; +} + +function $HttpParamSerializerJQLikeProvider() { + /** + * @ngdoc service + * @name $httpParamSerializerJQLike + * @description + * + * Alternative {@link $http `$http`} params serializer that follows + * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic. + * The serializer will also sort the params alphabetically. + * + * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property: + * + * ```js + * $http({ + * url: myUrl, + * method: 'GET', + * params: myParams, + * paramSerializer: '$httpParamSerializerJQLike' + * }); + * ``` + * + * It is also possible to set it as the default `paramSerializer` in the + * {@link $httpProvider#defaults `$httpProvider`}. + * + * Additionally, you can inject the serializer and use it explicitly, for example to serialize + * form data for submission: + * + * ```js + * .controller(function($http, $httpParamSerializerJQLike) { + * //... + * + * $http({ + * url: myUrl, + * method: 'POST', + * data: $httpParamSerializerJQLike(myData), + * headers: { + * 'Content-Type': 'application/x-www-form-urlencoded' + * } + * }); + * + * }); + * ``` + * + * */ + this.$get = function() { + return function jQueryLikeParamSerializer(params) { + if (!params) return ''; + var parts = []; + serialize(params, '', true); + return parts.join('&'); + + function serialize(toSerialize, prefix, topLevel) { + if (toSerialize === null || isUndefined(toSerialize)) return; + if (isArray(toSerialize)) { + forEach(toSerialize, function(value, index) { + serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); + }); + } else if (isObject(toSerialize) && !isDate(toSerialize)) { + forEachSorted(toSerialize, function(value, key) { + serialize(value, prefix + + (topLevel ? '' : '[') + + key + + (topLevel ? '' : ']')); + }); + } else { + parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); + } + } + }; + }; +} function defaultHttpResponseTransform(data, headers) { if (isString(data)) { @@ -8667,19 +9720,24 @@ function isJsonLike(str) { * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { - var parsed = createMap(), key, val, i; - - if (!headers) return parsed; - - forEach(headers.split('\n'), function(line) { - i = line.indexOf(':'); - key = lowercase(trim(line.substr(0, i))); - val = trim(line.substr(i + 1)); + var parsed = createMap(), i; + function fillInParsed(key, val) { if (key) { parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } - }); + } + + if (isString(headers)) { + forEach(headers.split('\n'), function(line) { + i = line.indexOf(':'); + fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1))); + }); + } else if (isObject(headers)) { + forEach(headers, function(headerVal, headerKey) { + fillInParsed(lowercase(headerKey), trim(headerVal)); + }); + } return parsed; } @@ -8698,7 +9756,7 @@ function parseHeaders(headers) { * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { - var headersObj = isObject(headers) ? headers : undefined; + var headersObj; return function(name) { if (!headersObj) headersObj = parseHeaders(headers); @@ -8728,8 +9786,9 @@ function headersGetter(headers) { * @returns {*} Transformed data. */ function transformData(data, headers, status, fns) { - if (isFunction(fns)) + if (isFunction(fns)) { return fns(data, headers, status); + } forEach(fns, function(fn) { data = fn(data, headers, status); @@ -8758,10 +9817,9 @@ function $HttpProvider() { * * Object containing default values for all {@link ng.$http $http} requests. * - * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} - * that will provide the cache for all requests who set their `cache` property to `true`. - * If you set the `default.cache = false` then only requests that specify their own custom - * cache object will be cached. See {@link $http#caching $http Caching} for more information. + * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses + * by default. See {@link $http#caching $http Caching} for more information. * * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. * Defaults value is `'XSRF-TOKEN'`. @@ -8777,6 +9835,12 @@ function $HttpProvider() { * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * + * + * - **`defaults.paramSerializer`** - `{string|function(Object<string,string>):string}` - A function + * used to the prepare string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. + * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -8798,7 +9862,9 @@ function $HttpProvider() { }, xsrfCookieName: 'XSRF-TOKEN', - xsrfHeaderName: 'X-XSRF-TOKEN' + xsrfHeaderName: 'X-XSRF-TOKEN', + + paramSerializer: '$httpParamSerializer' }; var useApplyAsync = false; @@ -8812,7 +9878,7 @@ function $HttpProvider() { * significant performance improvement for bigger applications that make many HTTP requests * concurrently (common during application bootstrap). * - * Defaults to false. If no value is specifed, returns the current configured value. + * Defaults to false. If no value is specified, returns the current configured value. * * @param {boolean=} value If true, when requests are loaded, they will schedule a deferred * "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window @@ -8829,6 +9895,30 @@ function $HttpProvider() { return useApplyAsync; }; + var useLegacyPromise = true; + /** + * @ngdoc method + * @name $httpProvider#useLegacyPromiseExtensions + * @description + * + * Configure `$http` service to return promises without the shorthand methods `success` and `error`. + * This should be used to make sure that applications work without these methods. + * + * Defaults to true. If no value is specified, returns the current configured value. + * + * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useLegacyPromiseExtensions = function(value) { + if (isDefined(value)) { + useLegacyPromise = !!value; + return this; + } + return useLegacyPromise; + }; + /** * @ngdoc property * @name $httpProvider#interceptors @@ -8844,12 +9934,18 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', + function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { var defaultCache = $cacheFactory('$http'); /** + * Make sure that default param serializer is exposed as a function + */ + defaults.paramSerializer = isString(defaults.paramSerializer) ? + $injector.get(defaults.paramSerializer) : defaults.paramSerializer; + + /** * Interceptors stored in reverse order. Inner interceptors before outer interceptors. * The reversal is needed so that we can build up the interception chain around the * server request. @@ -8888,66 +9984,47 @@ function $HttpProvider() { * * * ## General usage - * The `$http` service is a function which takes a single argument — a configuration object — - * that is used to generate an HTTP request and returns a {@link ng.$q promise} - * with two $http specific methods: `success` and `error`. - * - * ```js - * // Simple GET request example : - * $http.get('/someUrl'). - * success(function(data, status, headers, config) { - * // this callback will be called asynchronously - * // when the response is available - * }). - * error(function(data, status, headers, config) { - * // called asynchronously if an error occurs - * // or server returns response with an error status. - * }); - * ``` + * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} — + * that is used to generate an HTTP request and returns a {@link ng.$q promise}. * * ```js - * // Simple POST request example (passing data) : - * $http.post('/someUrl', {msg:'hello word!'}). - * success(function(data, status, headers, config) { + * // Simple GET request example: + * $http({ + * method: 'GET', + * url: '/someUrl' + * }).then(function successCallback(response) { * // this callback will be called asynchronously * // when the response is available - * }). - * error(function(data, status, headers, config) { + * }, function errorCallback(response) { * // called asynchronously if an error occurs * // or server returns response with an error status. * }); * ``` * + * The response object has these properties: * - * Since the returned value of calling the $http function is a `promise`, you can also use - * the `then` method to register callbacks, and these callbacks will receive a single argument – - * an object representing the response. See the API signature and type info below for more - * details. + * - **data** – `{string|Object}` – The response body transformed with the transform + * functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. * * A response status code between 200 and 299 is considered a success status and * will result in the success callback being called. Note that if the response is a redirect, * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * - * ## Writing Unit Tests that use $http - * When unit testing (using {@link ngMock ngMock}), it is necessary to call - * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending - * request using trained responses. - * - * ``` - * $httpBackend.expectGET(...); - * $http.get(...); - * $httpBackend.flush(); - * ``` * * ## Shortcut methods * * Shortcut methods are also available. All shortcut methods require passing in the URL, and - * request data must be passed in for POST/PUT requests. + * request data must be passed in for POST/PUT requests. An optional config can be passed as the + * last argument. * * ```js - * $http.get('/someUrl').success(successCallback); - * $http.post('/someUrl', data).success(successCallback); + * $http.get('/someUrl', config).then(successCallback, errorCallback); + * $http.post('/someUrl', data, config).then(successCallback, errorCallback); * ``` * * Complete list of shortcut methods: @@ -8961,6 +10038,25 @@ function $HttpProvider() { * - {@link ng.$http#patch $http.patch} * * + * ## Writing Unit Tests that use $http + * When unit testing (using {@link ngMock ngMock}), it is necessary to call + * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending + * request using trained responses. + * + * ``` + * $httpBackend.expectGET(...); + * $http.get(...); + * $httpBackend.flush(); + * ``` + * + * ## Deprecation Notice + * <div class="alert alert-danger"> + * The `$http` legacy promise methods `success` and `error` have been deprecated. + * Use the standard `then` method instead. + * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to + * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. + * </div> + * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults @@ -8977,7 +10073,7 @@ function $HttpProvider() { * To add or overwrite these defaults, simply add or remove a property from these configuration * objects. To add headers for an HTTP method other than POST or PUT, simply add a new object * with the lowercased HTTP method name as the key, e.g. - * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. + * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`. * * The defaults can also be set at runtime via the `$http.defaults` object in the same * fashion. For example: @@ -9004,7 +10100,7 @@ function $HttpProvider() { * data: { test: 'test' } * } * - * $http(req).success(function(){...}).error(function(){...}); + * $http(req).then(function(){...}, function(){...}); * ``` * * ## Transforming Requests and Responses @@ -9014,6 +10110,15 @@ function $HttpProvider() { * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, * which allows you to `push` or `unshift` a new transformation function into the transformation chain. * + * <div class="alert alert-warning"> + * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent his, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + * </div> + * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and @@ -9071,26 +10176,35 @@ function $HttpProvider() { * * ## Caching * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. + * + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object + * + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory($http)` object is used. + * + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. + * Take note that: * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. * * ## Interceptors * @@ -9110,7 +10224,7 @@ function $HttpProvider() { * * There are two kinds of interceptors (and two kinds of rejection interceptors): * - * * `request`: interceptors get called with a http `config` object. The function is free to + * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to * modify the `config` object or create a new one. The function needs to return the `config` * object directly, or a promise containing the `config` or a new `config` object. * * `requestError`: interceptor gets called when a previous interceptor threw an error or @@ -9212,13 +10326,13 @@ function $HttpProvider() { * * ### Cross Site Request Forgery (XSRF) Protection * - * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by + * which the attacker can trick an authenticated user into unknowingly executing actions on your + * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the + * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP + * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the + * cookie, your server can be assured that the XHR came from JavaScript running on your domain. + * The header will not be set for cross-domain requests. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the @@ -9233,19 +10347,20 @@ function $HttpProvider() { * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, * or the per-request config object. * + * In order to prevent collisions in environments where multiple Angular apps share the + * same domain or subdomain, we recommend that each application uses unique cookie name. * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. - * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned - * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be - * JSONified. + * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized + * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the - * header will not be sent. + * header will not be sent. Functions accept a config object as an argument. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – @@ -9260,32 +10375,27 @@ function $HttpProvider() { * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request * Overriding the Default Transformations} - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to + * prepare the string representation of request parameters (specified as an object). + * If specified as string, it is interpreted as function registered with the + * {@link $injector $injector}, which means you can create your own serializer + * by registering it as a {@link auto.$provide#service service}. + * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; + * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the * XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials) * for more information. * - **responseType** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). * - * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the - * standard `then` method and two http specific methods: `success` and `error`. The `then` - * method takes two arguments a success and an error callback which will be called with a - * response object. The `success` and `error` methods take a single argument - a function that - * will be called when the request succeeds or fails respectively. The arguments passed into - * these functions are destructured representation of the response object passed into the - * `then` method. The response object has these properties: + * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object + * when the request succeeds or fails. * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - **statusText** – `{string}` – HTTP status text of the response. * * @property {Array.<Object>} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. @@ -9295,11 +10405,11 @@ function $HttpProvider() { <example module="httpExample"> <file name="index.html"> <div ng-controller="FetchController"> - <select ng-model="method"> + <select ng-model="method" aria-label="Request method"> <option>GET</option> <option>JSONP</option> </select> - <input type="text" ng-model="url" size="80"/> + <input type="text" ng-model="url" size="80" aria-label="URL" /> <button id="fetchbtn" ng-click="fetch()">fetch</button><br> <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> <button id="samplejsonpbtn" @@ -9327,13 +10437,12 @@ function $HttpProvider() { $scope.response = null; $http({method: $scope.method, url: $scope.url, cache: $templateCache}). - success(function(data, status) { - $scope.status = status; - $scope.data = data; - }). - error(function(data, status) { - $scope.data = data || "Request failed"; - $scope.status = status; + then(function(response) { + $scope.status = response.status; + $scope.data = response.data; + }, function(response) { + $scope.data = response.data || "Request failed"; + $scope.status = response.status; }); }; @@ -9385,14 +10494,21 @@ function $HttpProvider() { throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } + if (!isString(requestConfig.url)) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url); + } + var config = extend({ method: 'get', transformRequest: defaults.transformRequest, - transformResponse: defaults.transformResponse + transformResponse: defaults.transformResponse, + paramSerializer: defaults.paramSerializer }, requestConfig); config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); + config.paramSerializer = isString(config.paramSerializer) ? + $injector.get(config.paramSerializer) : config.paramSerializer; var serverRequest = function(config) { var headers = config.headers; @@ -9435,45 +10551,47 @@ function $HttpProvider() { promise = promise.then(thenFn, rejectFn); } - promise.success = function(fn) { - assertArgFn(fn, 'fn'); + if (useLegacyPromise) { + promise.success = function(fn) { + assertArgFn(fn, 'fn'); - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; - promise.error = function(fn) { - assertArgFn(fn, 'fn'); + promise.error = function(fn) { + assertArgFn(fn, 'fn'); - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + } else { + promise.success = $httpMinErrLegacyFn('success'); + promise.error = $httpMinErrLegacyFn('error'); + } return promise; function transformResponse(response) { // make a copy since the response must be cacheable var resp = extend({}, response); - if (!response.data) { - resp.data = response.data; - } else { - resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); - } + resp.data = transformData(response.data, response.headers, response.status, + config.transformResponse); return (isSuccess(response.status)) ? resp : $q.reject(resp); } - function executeHeaderFns(headers) { + function executeHeaderFns(headers, config) { var headerContent, processedHeaders = {}; forEach(headers, function(headerFn, header) { if (isFunction(headerFn)) { - headerContent = headerFn(); + headerContent = headerFn(config); if (headerContent != null) { processedHeaders[header] = headerContent; } @@ -9507,7 +10625,7 @@ function $HttpProvider() { } // execute if header value is a function for merged headers - return executeHeaderFns(reqHeaders); + return executeHeaderFns(reqHeaders, shallowCopy(config)); } } @@ -9622,7 +10740,7 @@ function $HttpProvider() { function createShortMethods(names) { forEach(arguments, function(name) { $http[name] = function(url, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url })); @@ -9634,7 +10752,7 @@ function $HttpProvider() { function createShortMethodsWithData(name) { forEach(arguments, function(name) { $http[name] = function(url, data, config) { - return $http(extend(config || {}, { + return $http(extend({}, config || {}, { method: name, url: url, data: data @@ -9656,7 +10774,7 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.params); + url = buildUrl(config.url, config.paramSerializer(config.params)); $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); @@ -9694,7 +10812,7 @@ function $HttpProvider() { // send the request to the backend if (isUndefined(cachedResp)) { var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; @@ -9740,8 +10858,8 @@ function $HttpProvider() { * Resolves the raw $http promise. */ function resolvePromise(response, status, headers, statusText) { - // normalize internal statuses to 0 - status = Math.max(status, 0); + //status: HTTP response status code, 0, -1 (aborted by timeout / promise) + status = status >= -1 ? status : 0; (isSuccess(status) ? deferred.resolve : deferred.reject)({ data: response, @@ -9763,35 +10881,42 @@ function $HttpProvider() { } - function buildUrl(url, params) { - if (!params) return url; - var parts = []; - forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; - if (!isArray(value)) value = [value]; - - forEach(value, function(v) { - if (isObject(v)) { - if (isDate(v)) { - v = v.toISOString(); - } else { - v = toJson(v); - } - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if (parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + function buildUrl(url, serializedParams) { + if (serializedParams.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams; } return url; } }]; } -function createXhr() { - return new window.XMLHttpRequest(); +/** + * @ngdoc service + * @name $xhrFactory + * + * @description + * Factory function used to create XMLHttpRequest objects. + * + * Replace or decorate this service to create your own custom XMLHttpRequest objects. + * + * ``` + * angular.module('myApp', []) + * .factory('$xhrFactory', function() { + * return function createXhr(method, url) { + * return new window.XMLHttpRequest({mozSystem: true}); + * }; + * }); + * ``` + * + * @param {string} method HTTP method of the request (GET, POST, PUT, ..) + * @param {string} url URL of the request. + */ +function $xhrFactoryProvider() { + this.$get = function() { + return function createXhr() { + return new window.XMLHttpRequest(); + }; + }; } /** @@ -9799,6 +10924,7 @@ function createXhr() { * @name $httpBackend * @requires $window * @requires $document + * @requires $xhrFactory * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -9811,8 +10937,8 @@ function createXhr() { * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { - return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]); + this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) { + return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]); }]; } @@ -9836,7 +10962,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc }); } else { - var xhr = createXhr(); + var xhr = createXhr(method, url); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -9899,7 +11025,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } } - xhr.send(post || null); + xhr.send(isUndefined(post) ? null : post); } if (timeout > 0) { @@ -9916,7 +11042,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution - if (timeoutId !== undefined) { + if (isDefined(timeoutId)) { $browserDefer.cancel(timeoutId); } jsonpDone = xhr = null; @@ -9963,7 +11089,17 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } } -var $interpolateMinErr = minErr('$interpolate'); +var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate'); +$interpolateMinErr.throwNoconcat = function(text) { + throw $interpolateMinErr('noconcat', + "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + + "interpolations that concatenate multiple expressions when a trusted value is " + + "required. See http://docs.angularjs.org/api/ng.$sce", text); +}; + +$interpolateMinErr.interr = function(text, err) { + return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); +}; /** * @ngdoc provider @@ -9974,7 +11110,7 @@ var $interpolateMinErr = minErr('$interpolate'); * Used for configuring the interpolation markup. Defaults to `{{` and `}}`. * * @example -<example module="customInterpolationApp"> +<example name="custom-interpolation-markup" module="customInterpolationApp"> <file name="index.html"> <script> var customInterpolationApp = angular.module('customInterpolationApp', []); @@ -9989,7 +11125,7 @@ var $interpolateMinErr = minErr('$interpolate'); this.label = "This binding is brought you by // interpolation symbols."; }); </script> -<div ng-app="App" ng-controller="DemoController as demo"> +<div ng-controller="DemoController as demo"> //demo.label// </div> </file> @@ -10051,6 +11187,28 @@ function $InterpolateProvider() { return '\\\\\\' + ch; } + function unescapeText(text) { + return text.replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + } + + function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + value = toJson(value); + } + + return value; + } + /** * @ngdoc service * @name $interpolate @@ -10070,7 +11228,7 @@ function $InterpolateProvider() { * ```js * var $interpolate = ...; // injected * var exp = $interpolate('Hello {{name | uppercase}}!'); - * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); + * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!'); * ``` * * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is @@ -10185,10 +11343,7 @@ function $InterpolateProvider() { // make it obvious that you bound the value to some user controlled value. This helps reduce // the load when auditing for XSS issues. if (trustedContext && concat.length > 1) { - throw $interpolateMinErr('noconcat', - "Error while interpolating: {0}\nStrict Contextual Escaping disallows " + - "interpolations that concatenate multiple expressions when a trusted value is " + - "required. See http://docs.angularjs.org/api/ng.$sce", text); + $interpolateMinErr.throwNoconcat(text); } if (!mustHaveExpression || expressions.length) { @@ -10206,23 +11361,6 @@ function $InterpolateProvider() { $sce.valueOf(value); }; - var stringify = function(value) { - if (value == null) { // null || undefined - return ''; - } - switch (typeof value) { - case 'string': - break; - case 'number': - value = '' + value; - break; - default: - value = toJson(value); - } - - return value; - }; - return extend(function interpolationFn(context) { var i = 0; var ii = expressions.length; @@ -10235,16 +11373,14 @@ function $InterpolateProvider() { return compute(values); } catch (err) { - var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, - err.toString()); - $exceptionHandler(newErr); + $exceptionHandler($interpolateMinErr.interr(text, err)); } }, { // all of these properties are undocumented for now exp: text, //just for compatibility with regular watchers created via $watch expressions: expressions, - $$watchDelegate: function(scope, listener, objectEquality) { + $$watchDelegate: function(scope, listener) { var lastValue; return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { var currValue = compute(values); @@ -10252,24 +11388,17 @@ function $InterpolateProvider() { listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); } lastValue = currValue; - }, objectEquality); + }); } }); } - function unescapeText(text) { - return text.replace(escapedStartRegexp, startSymbol). - replace(escapedEndRegexp, endSymbol); - } - function parseStringifyInterceptor(value) { try { value = getValue(value); return allOrNothing && !isDefined(value) ? value : stringify(value); } catch (err) { - var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, - err.toString()); - $exceptionHandler(newErr); + $exceptionHandler($interpolateMinErr.interr(text, err)); } } } @@ -10348,6 +11477,7 @@ function $IntervalProvider() { * indefinitely. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. * @returns {promise} A promise which will be notified on each iteration. * * @example @@ -10426,7 +11556,7 @@ function $IntervalProvider() { * * <div> * <div ng-controller="ExampleController"> - * Date format: <input ng-model="format"> <hr/> + * <label>Date format: <input ng-model="format"></label> <hr/> * Current time is: <span my-current-time="format"></span> * <hr/> * Blood 1 : <font color='red'>{{blood_1}}</font> @@ -10441,7 +11571,9 @@ function $IntervalProvider() { * </example> */ function interval(fn, delay, count, invokeApply) { - var setInterval = $window.setInterval, + var hasParams = arguments.length > 4, + args = hasParams ? sliceArgs(arguments, 4) : [], + setInterval = $window.setInterval, clearInterval = $window.clearInterval, iteration = 0, skipApply = (isDefined(invokeApply) && !invokeApply), @@ -10450,7 +11582,9 @@ function $IntervalProvider() { count = isDefined(count) ? count : 0; - promise.then(null, null, fn); + promise.then(null, null, (!hasParams) ? fn : function() { + fn.apply(null, args); + }); promise.$$intervalId = setInterval(function tick() { deferred.notify(iteration++); @@ -10478,7 +11612,7 @@ function $IntervalProvider() { * @description * Cancels a task associated with the `promise`. * - * @param {promise} promise returned by the `$interval` function. + * @param {Promise=} promise returned by the `$interval` function. * @returns {boolean} Returns `true` if the task was successfully canceled. */ interval.cancel = function(promise) { @@ -10505,75 +11639,6 @@ function $IntervalProvider() { * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -function $LocaleProvider() { - this.$get = function() { - return { - id: 'en-us', - - NUMBER_FORMATS: { - DECIMAL_SEP: '.', - GROUP_SEP: ',', - PATTERNS: [ - { // Decimal Pattern - minInt: 1, - minFrac: 0, - maxFrac: 3, - posPre: '', - posSuf: '', - negPre: '-', - negSuf: '', - gSize: 3, - lgSize: 3 - },{ //Currency Pattern - minInt: 1, - minFrac: 2, - maxFrac: 2, - posPre: '\u00A4', - posSuf: '', - negPre: '(\u00A4', - negSuf: ')', - gSize: 3, - lgSize: 3 - } - ], - CURRENCY_SYM: '$' - }, - - DATETIME_FORMATS: { - MONTH: - 'January,February,March,April,May,June,July,August,September,October,November,December' - .split(','), - SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), - DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), - SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), - AMPMS: ['AM','PM'], - medium: 'MMM d, y h:mm:ss a', - 'short': 'M/d/yy h:mm a', - fullDate: 'EEEE, MMMM d, y', - longDate: 'MMMM d, y', - mediumDate: 'MMM d, y', - shortDate: 'M/d/yy', - mediumTime: 'h:mm:ss a', - shortTime: 'h:mm a', - ERANAMES: [ - "Before Christ", - "Anno Domini" - ], - ERAS: [ - "BC", - "AD" - ] - }, - - pluralCat: function(num) { - if (num === 1) { - return 'one'; - } - return 'other'; - } - }; - }; -} var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; @@ -10602,7 +11667,7 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; - locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; + locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } @@ -10664,12 +11729,12 @@ function serverBase(url) { * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} basePrefix url path prefix */ -function LocationHtml5Url(appBase, basePrefix) { +function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; - var appBaseNoFile = stripFile(appBase); parseAbsoluteUrl(appBase, this); @@ -10716,14 +11781,14 @@ function LocationHtml5Url(appBase, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; - if ((appUrl = beginsWith(appBase, url)) !== undefined) { + if (isDefined(appUrl = beginsWith(appBase, url))) { prevAppUrl = appUrl; - if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { + if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) { rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } - } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { + } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) { rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { rewrittenUrl = appBaseNoFile; @@ -10743,10 +11808,10 @@ function LocationHtml5Url(appBase, basePrefix) { * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangUrl(appBase, hashPrefix) { - var appBaseNoFile = stripFile(appBase); +function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { parseAbsoluteUrl(appBase, this); @@ -10855,14 +11920,13 @@ function LocationHashbangUrl(appBase, hashPrefix) { * * @constructor * @param {string} appBase application base URL + * @param {string} appBaseNoFile application base URL stripped of any filename * @param {string} hashPrefix hashbang prefix */ -function LocationHashbangInHtml5Url(appBase, hashPrefix) { +function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { this.$$html5 = true; LocationHashbangUrl.apply(this, arguments); - var appBaseNoFile = stripFile(appBase); - this.$$parseLinkUrl = function(url, relHref) { if (relHref && relHref[0] === '#') { // special case for links to hash fragments: @@ -10956,8 +12020,9 @@ var locationPrototype = { * @return {string} url */ url: function(url) { - if (isUndefined(url)) + if (isUndefined(url)) { return this.$$url; + } var match = PATH_MATCH.exec(url); if (match[1] || url === '') this.path(decodeURIComponent(match[1])); @@ -11149,9 +12214,9 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return hash fragment when called without any parameter. + * Returns the hash fragment when called without any parameters. * - * Change hash fragment when called with parameter and return `$location`. + * Changes the hash fragment when called with a parameter and returns `$location`. * * * ```js @@ -11172,8 +12237,8 @@ var locationPrototype = { * @name $location#replace * * @description - * If called, all changes to $location during current `$digest` will be replacing current history - * record, instead of adding new one. + * If called, all changes to $location during the current `$digest` will replace the current history + * record, instead of adding a new one. */ replace: function() { this.$$replace = true; @@ -11204,8 +12269,9 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun * @return {object} state */ Location.prototype.state = function(state) { - if (!arguments.length) + if (!arguments.length) { return this.$$state; + } if (Location !== LocationHtml5Url || !this.$$html5) { throw $locationMinErr('nostate', 'History API state support is available only ' + @@ -11230,8 +12296,9 @@ function locationGetter(property) { function locationGetterSetter(property, preprocess) { return function(value) { - if (isUndefined(value)) + if (isUndefined(value)) { return this[property]; + } this[property] = preprocess(value); this.$$compose(); @@ -11398,7 +12465,9 @@ function $LocationProvider() { appBase = stripHash(initialUrl); LocationMode = LocationHashbangUrl; } - $location = new LocationMode(appBase, '#' + hashPrefix); + var appBaseNoFile = stripFile(appBase); + + $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix); $location.$$parseLinkUrl(initialUrl, initialUrl); $location.$$state = $browser.state(); @@ -11478,11 +12547,18 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { + + if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { + // If we are navigating outside of the app then force a reload + $window.location.href = newUrl; + return; + } + $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; - + newUrl = trimEmptyHash(newUrl); $location.$$parse(newUrl); $location.$$state = newState; @@ -11580,8 +12656,8 @@ function $LocationProvider() { <file name="index.html"> <div ng-controller="LogController"> <p>Reload this page with open console, enter text and hit the log button...</p> - Message: - <input type="text" ng-model="message"/> + <label>Message: + <input type="text" ng-model="message" /></label> <button ng-click="$log.log(message)">log</button> <button ng-click="$log.warn(message)">warn</button> <button ng-click="$log.info(message)">info</button> @@ -11765,6 +12841,25 @@ function ensureSafeMemberName(name, fullExpression) { return name; } +function getStringValue(name, fullExpression) { + // From the JavaScript docs: + // Property names must be strings. This means that non-string objects cannot be used + // as keys in an object. Any non-string object, including a number, is typecasted + // into a string via the toString method. + // + // So, to ensure that we are checking the same `name` that JavaScript would use, + // we cast it to a string, if possible. + // Doing `name + ''` can cause a repl error if the result to `toString` is not a string, + // this is, this will handle objects that misbehave. + name = name + ''; + if (!isString(name)) { + throw $parseMinErr('iseccst', + 'Cannot convert object to primitive value! ' + + 'Expression: {0}', fullExpression); + } + return name; +} + function ensureSafeObject(obj, fullExpression) { // nifty check if obj is Function that is fast and works across iframes and other contexts if (obj) { @@ -11810,57 +12905,18 @@ function ensureSafeFunction(obj, fullExpression) { } } -//Keyword constants -var CONSTANTS = createMap(); -forEach({ - 'null': function() { return null; }, - 'true': function() { return true; }, - 'false': function() { return false; }, - 'undefined': function() {} -}, function(constantGetter, name) { - constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true; - CONSTANTS[name] = constantGetter; -}); - -//Not quite a constant, but can be lex/parsed the same -CONSTANTS['this'] = function(self) { return self; }; -CONSTANTS['this'].sharedGetter = true; - +function ensureSafeAssignContext(obj, fullExpression) { + if (obj) { + if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor || + obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { + throw $parseMinErr('isecaf', + 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression); + } + } +} -//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter -var OPERATORS = extend(createMap(), { - '+':function(self, locals, a, b) { - a=a(self, locals); b=b(self, locals); - if (isDefined(a)) { - if (isDefined(b)) { - return a + b; - } - return a; - } - return isDefined(b) ? b : undefined;}, - '-':function(self, locals, a, b) { - a=a(self, locals); b=b(self, locals); - return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0); - }, - '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);}, - '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);}, - '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);}, - '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);}, - '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);}, - '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);}, - '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);}, - '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);}, - '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);}, - '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);}, - '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);}, - '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);}, - '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);}, - '!':function(self, locals, a) {return !a(self, locals);}, - - //Tokenized as operators but parsed as assignment/filters - '=':true, - '|':true -}); +var OPERATORS = createMap(); +forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -12012,8 +13068,9 @@ Lexer.prototype = { if (escape) { if (ch === 'u') { var hex = this.text.substring(this.index + 1, this.index + 5); - if (!hex.match(/[\da-f]{4}/i)) + if (!hex.match(/[\da-f]{4}/i)) { this.throwError('Invalid unicode escape [\\u' + hex + ']'); + } this.index += 4; string += String.fromCharCode(parseInt(hex, 16)); } else { @@ -12041,194 +13098,59 @@ Lexer.prototype = { } }; - -function isConstant(exp) { - return exp.constant; -} - -/** - * @constructor - */ -var Parser = function(lexer, $filter, options) { +var AST = function(lexer, options) { this.lexer = lexer; - this.$filter = $filter; this.options = options; }; -Parser.ZERO = extend(function() { - return 0; -}, { - sharedGetter: true, - constant: true -}); - -Parser.prototype = { - constructor: Parser, - - parse: function(text) { +AST.Program = 'Program'; +AST.ExpressionStatement = 'ExpressionStatement'; +AST.AssignmentExpression = 'AssignmentExpression'; +AST.ConditionalExpression = 'ConditionalExpression'; +AST.LogicalExpression = 'LogicalExpression'; +AST.BinaryExpression = 'BinaryExpression'; +AST.UnaryExpression = 'UnaryExpression'; +AST.CallExpression = 'CallExpression'; +AST.MemberExpression = 'MemberExpression'; +AST.Identifier = 'Identifier'; +AST.Literal = 'Literal'; +AST.ArrayExpression = 'ArrayExpression'; +AST.Property = 'Property'; +AST.ObjectExpression = 'ObjectExpression'; +AST.ThisExpression = 'ThisExpression'; + +// Internal use only +AST.NGValueParameter = 'NGValueParameter'; + +AST.prototype = { + ast: function(text) { this.text = text; this.tokens = this.lexer.lex(text); - var value = this.statements(); + var value = this.program(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); } - value.literal = !!value.literal; - value.constant = !!value.constant; - return value; }, - primary: function() { - var primary; - if (this.expect('(')) { - primary = this.filterChain(); - this.consume(')'); - } else if (this.expect('[')) { - primary = this.arrayDeclaration(); - } else if (this.expect('{')) { - primary = this.object(); - } else if (this.peek().identifier && this.peek().text in CONSTANTS) { - primary = CONSTANTS[this.consume().text]; - } else if (this.peek().identifier) { - primary = this.identifier(); - } else if (this.peek().constant) { - primary = this.constant(); - } else { - this.throwError('not a primary expression', this.peek()); - } - - var next, context; - while ((next = this.expect('(', '[', '.'))) { - if (next.text === '(') { - primary = this.functionCall(primary, context); - context = null; - } else if (next.text === '[') { - context = primary; - primary = this.objectIndex(primary); - } else if (next.text === '.') { - context = primary; - primary = this.fieldAccess(primary); - } else { - this.throwError('IMPOSSIBLE'); - } - } - return primary; - }, - - throwError: function(msg, token) { - throw $parseMinErr('syntax', - 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', - token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); - }, - - peekToken: function() { - if (this.tokens.length === 0) - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - return this.tokens[0]; - }, - - peek: function(e1, e2, e3, e4) { - return this.peekAhead(0, e1, e2, e3, e4); - }, - peekAhead: function(i, e1, e2, e3, e4) { - if (this.tokens.length > i) { - var token = this.tokens[i]; - var t = token.text; - if (t === e1 || t === e2 || t === e3 || t === e4 || - (!e1 && !e2 && !e3 && !e4)) { - return token; - } - } - return false; - }, - - expect: function(e1, e2, e3, e4) { - var token = this.peek(e1, e2, e3, e4); - if (token) { - this.tokens.shift(); - return token; - } - return false; - }, - - consume: function(e1) { - if (this.tokens.length === 0) { - throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); - } - - var token = this.expect(e1); - if (!token) { - this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); - } - return token; - }, - - unaryFn: function(op, right) { - var fn = OPERATORS[op]; - return extend(function $parseUnaryFn(self, locals) { - return fn(self, locals, right); - }, { - constant:right.constant, - inputs: [right] - }); - }, - - binaryFn: function(left, op, right, isBranching) { - var fn = OPERATORS[op]; - return extend(function $parseBinaryFn(self, locals) { - return fn(self, locals, left, right); - }, { - constant: left.constant && right.constant, - inputs: !isBranching && [left, right] - }); - }, - - identifier: function() { - var id = this.consume().text; - - //Continue reading each `.identifier` unless it is a method invocation - while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) { - id += this.consume().text + this.consume().text; - } - - return getterFn(id, this.options, this.text); - }, - - constant: function() { - var value = this.consume().value; - - return extend(function $parseConstant() { - return value; - }, { - constant: true, - literal: true - }); - }, - - statements: function() { - var statements = []; + program: function() { + var body = []; while (true) { if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) - statements.push(this.filterChain()); + body.push(this.expressionStatement()); if (!this.expect(';')) { - // optimize for the common case where there is only one statement. - // TODO(size): maybe we should not support multiple statements? - return (statements.length === 1) - ? statements[0] - : function $parseStatements(self, locals) { - var value; - for (var i = 0, ii = statements.length; i < ii; i++) { - value = statements[i](self, locals); - } - return value; - }; + return { type: AST.Program, body: body}; } } }, + expressionStatement: function() { + return { type: AST.ExpressionStatement, expression: this.filterChain() }; + }, + filterChain: function() { var left = this.expression(); var token; @@ -12238,98 +13160,44 @@ Parser.prototype = { return left; }, - filter: function(inputFn) { - var fn = this.$filter(this.consume().text); - var argsFn; - var args; - - if (this.peek(':')) { - argsFn = []; - args = []; // we can safely reuse the array - while (this.expect(':')) { - argsFn.push(this.expression()); - } - } - - var inputs = [inputFn].concat(argsFn || []); - - return extend(function $parseFilter(self, locals) { - var input = inputFn(self, locals); - if (args) { - args[0] = input; - - var i = argsFn.length; - while (i--) { - args[i + 1] = argsFn[i](self, locals); - } - - return fn.apply(undefined, args); - } - - return fn(input); - }, { - constant: !fn.$stateful && inputs.every(isConstant), - inputs: !fn.$stateful && inputs - }); - }, - expression: function() { return this.assignment(); }, assignment: function() { - var left = this.ternary(); - var right; - var token; - if ((token = this.expect('='))) { - if (!left.assign) { - this.throwError('implies assignment but [' + - this.text.substring(0, token.index) + '] can not be assigned to', token); - } - right = this.ternary(); - return extend(function $parseAssignment(scope, locals) { - return left.assign(scope, right(scope, locals), locals); - }, { - inputs: [left, right] - }); + var result = this.ternary(); + if (this.expect('=')) { + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } - return left; + return result; }, ternary: function() { - var left = this.logicalOR(); - var middle; - var token; - if ((token = this.expect('?'))) { - middle = this.assignment(); + var test = this.logicalOR(); + var alternate; + var consequent; + if (this.expect('?')) { + alternate = this.expression(); if (this.consume(':')) { - var right = this.assignment(); - - return extend(function $parseTernary(self, locals) { - return left(self, locals) ? middle(self, locals) : right(self, locals); - }, { - constant: left.constant && middle.constant && right.constant - }); + consequent = this.expression(); + return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent}; } } - - return left; + return test; }, logicalOR: function() { var left = this.logicalAND(); - var token; - while ((token = this.expect('||'))) { - left = this.binaryFn(left, token.text, this.logicalAND(), true); + while (this.expect('||')) { + left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() }; } return left; }, logicalAND: function() { var left = this.equality(); - var token; - while ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.text, this.equality(), true); + while (this.expect('&&')) { + left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()}; } return left; }, @@ -12338,7 +13206,7 @@ Parser.prototype = { var left = this.relational(); var token; while ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.text, this.relational()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() }; } return left; }, @@ -12347,7 +13215,7 @@ Parser.prototype = { var left = this.additive(); var token; while ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.text, this.additive()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() }; } return left; }, @@ -12356,7 +13224,7 @@ Parser.prototype = { var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.text, this.multiplicative()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() }; } return left; }, @@ -12365,318 +13233,1237 @@ Parser.prototype = { var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.text, this.unary()); + left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() }; } return left; }, unary: function() { var token; - if (this.expect('+')) { - return this.primary(); - } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.text, this.unary()); - } else if ((token = this.expect('!'))) { - return this.unaryFn(token.text, this.unary()); + if ((token = this.expect('+', '-', '!'))) { + return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() }; } else { return this.primary(); } }, - fieldAccess: function(object) { - var getter = this.identifier(); + primary: function() { + var primary; + if (this.expect('(')) { + primary = this.filterChain(); + this.consume(')'); + } else if (this.expect('[')) { + primary = this.arrayDeclaration(); + } else if (this.expect('{')) { + primary = this.object(); + } else if (this.constants.hasOwnProperty(this.peek().text)) { + primary = copy(this.constants[this.consume().text]); + } else if (this.peek().identifier) { + primary = this.identifier(); + } else if (this.peek().constant) { + primary = this.constant(); + } else { + this.throwError('not a primary expression', this.peek()); + } - return extend(function $parseFieldAccess(scope, locals, self) { - var o = self || object(scope, locals); - return (o == null) ? undefined : getter(o); - }, { - assign: function(scope, value, locals) { - var o = object(scope, locals); - if (!o) object.assign(scope, o = {}, locals); - return getter.assign(o, value); + var next; + while ((next = this.expect('(', '[', '.'))) { + if (next.text === '(') { + primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() }; + this.consume(')'); + } else if (next.text === '[') { + primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true }; + this.consume(']'); + } else if (next.text === '.') { + primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false }; + } else { + this.throwError('IMPOSSIBLE'); } - }); + } + return primary; }, - objectIndex: function(obj) { - var expression = this.text; + filter: function(baseExpression) { + var args = [baseExpression]; + var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true}; - var indexFn = this.expression(); - this.consume(']'); + while (this.expect(':')) { + args.push(this.expression()); + } - return extend(function $parseObjectIndex(self, locals) { - var o = obj(self, locals), - i = indexFn(self, locals), - v; - - ensureSafeMemberName(i, expression); - if (!o) return undefined; - v = ensureSafeObject(o[i], expression); - return v; - }, { - assign: function(self, value, locals) { - var key = ensureSafeMemberName(indexFn(self, locals), expression); - // prevent overwriting of Function.constructor which would break ensureSafeObject check - var o = ensureSafeObject(obj(self, locals), expression); - if (!o) obj.assign(self, o = {}, locals); - return o[key] = value; - } - }); + return result; }, - functionCall: function(fnGetter, contextGetter) { - var argsFn = []; + parseArguments: function() { + var args = []; if (this.peekToken().text !== ')') { do { - argsFn.push(this.expression()); + args.push(this.expression()); } while (this.expect(',')); } - this.consume(')'); - - var expressionText = this.text; - // we can safely reuse the array across invocations - var args = argsFn.length ? [] : null; - - return function $parseFunctionCall(scope, locals) { - var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; - var fn = fnGetter(scope, locals, context) || noop; - - if (args) { - var i = argsFn.length; - while (i--) { - args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); - } - } - - ensureSafeObject(context, expressionText); - ensureSafeFunction(fn, expressionText); - - // IE doesn't have apply for some native functions - var v = fn.apply - ? fn.apply(context, args) - : fn(args[0], args[1], args[2], args[3], args[4]); + return args; + }, - if (args) { - // Free-up the memory (arguments of the last function call). - args.length = 0; - } + identifier: function() { + var token = this.consume(); + if (!token.identifier) { + this.throwError('is not a valid identifier', token); + } + return { type: AST.Identifier, name: token.text }; + }, - return ensureSafeObject(v, expressionText); - }; + constant: function() { + // TODO check that it is a constant + return { type: AST.Literal, value: this.consume().value }; }, - // This is used with json array declaration arrayDeclaration: function() { - var elementFns = []; + var elements = []; if (this.peekToken().text !== ']') { do { if (this.peek(']')) { // Support trailing commas per ES5.1. break; } - elementFns.push(this.expression()); + elements.push(this.expression()); } while (this.expect(',')); } this.consume(']'); - return extend(function $parseArrayLiteral(self, locals) { - var array = []; - for (var i = 0, ii = elementFns.length; i < ii; i++) { - array.push(elementFns[i](self, locals)); - } - return array; - }, { - literal: true, - constant: elementFns.every(isConstant), - inputs: elementFns - }); + return { type: AST.ArrayExpression, elements: elements }; }, object: function() { - var keys = [], valueFns = []; + var properties = [], property; if (this.peekToken().text !== '}') { do { if (this.peek('}')) { // Support trailing commas per ES5.1. break; } - var token = this.consume(); - if (token.constant) { - keys.push(token.value); - } else if (token.identifier) { - keys.push(token.text); + property = {type: AST.Property, kind: 'init'}; + if (this.peek().constant) { + property.key = this.constant(); + } else if (this.peek().identifier) { + property.key = this.identifier(); } else { - this.throwError("invalid key", token); + this.throwError("invalid key", this.peek()); } this.consume(':'); - valueFns.push(this.expression()); + property.value = this.expression(); + properties.push(property); } while (this.expect(',')); } this.consume('}'); - return extend(function $parseObjectLiteral(self, locals) { - var object = {}; - for (var i = 0, ii = valueFns.length; i < ii; i++) { - object[keys[i]] = valueFns[i](self, locals); + return {type: AST.ObjectExpression, properties: properties }; + }, + + throwError: function(msg, token) { + throw $parseMinErr('syntax', + 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].', + token.text, msg, (token.index + 1), this.text, this.text.substring(token.index)); + }, + + consume: function(e1) { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } + + var token = this.expect(e1); + if (!token) { + this.throwError('is unexpected, expecting [' + e1 + ']', this.peek()); + } + return token; + }, + + peekToken: function() { + if (this.tokens.length === 0) { + throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text); + } + return this.tokens[0]; + }, + + peek: function(e1, e2, e3, e4) { + return this.peekAhead(0, e1, e2, e3, e4); + }, + + peekAhead: function(i, e1, e2, e3, e4) { + if (this.tokens.length > i) { + var token = this.tokens[i]; + var t = token.text; + if (t === e1 || t === e2 || t === e3 || t === e4 || + (!e1 && !e2 && !e3 && !e4)) { + return token; } - return object; - }, { - literal: true, - constant: valueFns.every(isConstant), - inputs: valueFns - }); + } + return false; + }, + + expect: function(e1, e2, e3, e4) { + var token = this.peek(e1, e2, e3, e4); + if (token) { + this.tokens.shift(); + return token; + } + return false; + }, + + + /* `undefined` is not a constant, it is an identifier, + * but using it as an identifier is not supported + */ + constants: { + 'true': { type: AST.Literal, value: true }, + 'false': { type: AST.Literal, value: false }, + 'null': { type: AST.Literal, value: null }, + 'undefined': {type: AST.Literal, value: undefined }, + 'this': {type: AST.ThisExpression } } }; +function ifDefined(v, d) { + return typeof v !== 'undefined' ? v : d; +} -////////////////////////////////////////////////// -// Parser helper functions -////////////////////////////////////////////////// +function plusFn(l, r) { + if (typeof l === 'undefined') return r; + if (typeof r === 'undefined') return l; + return l + r; +} -function setter(obj, locals, path, setValue, fullExp) { - ensureSafeObject(obj, fullExp); - ensureSafeObject(locals, fullExp); +function isStateless($filter, filterName) { + var fn = $filter(filterName); + return !fn.$stateful; +} - var element = path.split('.'), key; - for (var i = 0; element.length > 1; i++) { - key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = (i === 0 && locals && locals[key]) || obj[key]; - if (!propertyObj) { - propertyObj = {}; - obj[key] = propertyObj; - } - obj = ensureSafeObject(propertyObj, fullExp); +function findConstantAndWatchExpressions(ast, $filter) { + var allConstants; + var argsToWatch; + switch (ast.type) { + case AST.Program: + allConstants = true; + forEach(ast.body, function(expr) { + findConstantAndWatchExpressions(expr.expression, $filter); + allConstants = allConstants && expr.expression.constant; + }); + ast.constant = allConstants; + break; + case AST.Literal: + ast.constant = true; + ast.toWatch = []; + break; + case AST.UnaryExpression: + findConstantAndWatchExpressions(ast.argument, $filter); + ast.constant = ast.argument.constant; + ast.toWatch = ast.argument.toWatch; + break; + case AST.BinaryExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); + break; + case AST.LogicalExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.ConditionalExpression: + findConstantAndWatchExpressions(ast.test, $filter); + findConstantAndWatchExpressions(ast.alternate, $filter); + findConstantAndWatchExpressions(ast.consequent, $filter); + ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; + ast.toWatch = ast.constant ? [] : [ast]; + break; + case AST.Identifier: + ast.constant = false; + ast.toWatch = [ast]; + break; + case AST.MemberExpression: + findConstantAndWatchExpressions(ast.object, $filter); + if (ast.computed) { + findConstantAndWatchExpressions(ast.property, $filter); + } + ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); + ast.toWatch = [ast]; + break; + case AST.CallExpression: + allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + argsToWatch = []; + forEach(ast.arguments, function(expr) { + findConstantAndWatchExpressions(expr, $filter); + allConstants = allConstants && expr.constant; + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + break; + case AST.AssignmentExpression: + findConstantAndWatchExpressions(ast.left, $filter); + findConstantAndWatchExpressions(ast.right, $filter); + ast.constant = ast.left.constant && ast.right.constant; + ast.toWatch = [ast]; + break; + case AST.ArrayExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.elements, function(expr) { + findConstantAndWatchExpressions(expr, $filter); + allConstants = allConstants && expr.constant; + if (!expr.constant) { + argsToWatch.push.apply(argsToWatch, expr.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ObjectExpression: + allConstants = true; + argsToWatch = []; + forEach(ast.properties, function(property) { + findConstantAndWatchExpressions(property.value, $filter); + allConstants = allConstants && property.value.constant; + if (!property.value.constant) { + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + } + }); + ast.constant = allConstants; + ast.toWatch = argsToWatch; + break; + case AST.ThisExpression: + ast.constant = false; + ast.toWatch = []; + break; } - key = ensureSafeMemberName(element.shift(), fullExp); - ensureSafeObject(obj[key], fullExp); - obj[key] = setValue; - return setValue; } -var getterFnCacheDefault = createMap(); -var getterFnCacheExpensive = createMap(); +function getInputs(body) { + if (body.length != 1) return; + var lastExpression = body[0].expression; + var candidate = lastExpression.toWatch; + if (candidate.length !== 1) return candidate; + return candidate[0] !== lastExpression ? candidate : undefined; +} -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; +function isAssignable(ast) { + return ast.type === AST.Identifier || ast.type === AST.MemberExpression; } -/** - * Implementation of the "Black Hole" variant from: - * - http://jsperf.com/angularjs-parse-getter/4 - * - http://jsperf.com/path-evaluation-simplified/7 - */ -function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - ensureSafeMemberName(key2, fullExp); - ensureSafeMemberName(key3, fullExp); - ensureSafeMemberName(key4, fullExp); - var eso = function(o) { - return ensureSafeObject(o, fullExp); - }; - var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity; - var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity; - var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity; - var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity; - var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity; +function assignableAST(ast) { + if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) { + return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='}; + } +} - return function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; +function isLiteral(ast) { + return ast.body.length === 0 || + ast.body.length === 1 && ( + ast.body[0].expression.type === AST.Literal || + ast.body[0].expression.type === AST.ArrayExpression || + ast.body[0].expression.type === AST.ObjectExpression); +} - if (pathVal == null) return pathVal; - pathVal = eso0(pathVal[key0]); +function isConstant(ast) { + return ast.constant; +} - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso1(pathVal[key1]); +function ASTCompiler(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso2(pathVal[key2]); +ASTCompiler.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.state = { + nextId: 0, + filters: {}, + expensiveChecks: expensiveChecks, + fn: {vars: [], body: [], own: {}}, + assign: {vars: [], body: [], own: {}}, + inputs: [] + }; + findConstantAndWatchExpressions(ast, self.$filter); + var extra = ''; + var assignable; + this.stage = 'assign'; + if ((assignable = assignableAST(ast))) { + this.state.computing = 'assign'; + var result = this.nextId(); + this.recurse(assignable, result); + this.return_(result); + extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l'); + } + var toWatch = getInputs(ast.body); + self.stage = 'inputs'; + forEach(toWatch, function(watch, key) { + var fnKey = 'fn' + key; + self.state[fnKey] = {vars: [], body: [], own: {}}; + self.state.computing = fnKey; + var intoId = self.nextId(); + self.recurse(watch, intoId); + self.return_(intoId); + self.state.inputs.push(fnKey); + watch.watchId = key; + }); + this.state.computing = 'fn'; + this.stage = 'main'; + this.recurse(ast); + var fnString = + // The build and minification steps remove the string "use strict" from the code, but this is done using a regex. + // This is a workaround for this until we do a better job at only removing the prefix only when we should. + '"' + this.USE + ' ' + this.STRICT + '";\n' + + this.filterPrefix() + + 'var fn=' + this.generateFunction('fn', 's,l,a,i') + + extra + + this.watchFns() + + 'return fn;'; - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso3(pathVal[key3]); + /* jshint -W054 */ + var fn = (new Function('$filter', + 'ensureSafeMemberName', + 'ensureSafeObject', + 'ensureSafeFunction', + 'getStringValue', + 'ensureSafeAssignContext', + 'ifDefined', + 'plus', + 'text', + fnString))( + this.$filter, + ensureSafeMemberName, + ensureSafeObject, + ensureSafeFunction, + getStringValue, + ensureSafeAssignContext, + ifDefined, + plusFn, + expression); + /* jshint +W054 */ + this.state = this.stage = undefined; + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; + }, - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = eso4(pathVal[key4]); + USE: 'use', - return pathVal; - }; -} + STRICT: 'strict', -function getterFnWithEnsureSafeObject(fn, fullExpression) { - return function(s, l) { - return fn(s, l, ensureSafeObject, fullExpression); - }; -} + watchFns: function() { + var result = []; + var fns = this.state.inputs; + var self = this; + forEach(fns, function(name) { + result.push('var ' + name + '=' + self.generateFunction(name, 's')); + }); + if (fns.length) { + result.push('fn.inputs=[' + fns.join(',') + '];'); + } + return result.join(''); + }, + + generateFunction: function(name, params) { + return 'function(' + params + '){' + + this.varsPrefix(name) + + this.body(name) + + '};'; + }, -function getterFn(path, options, fullExp) { - var expensiveChecks = options.expensiveChecks; - var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); - var fn = getterFnCache[path]; - if (fn) return fn; + filterPrefix: function() { + var parts = []; + var self = this; + forEach(this.state.filters, function(id, filter) { + parts.push(id + '=$filter(' + self.escape(filter) + ')'); + }); + if (parts.length) return 'var ' + parts.join(',') + ';'; + return ''; + }, + varsPrefix: function(section) { + return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : ''; + }, - var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length; + body: function(section) { + return this.state[section].body.join(''); + }, + + recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var left, right, self = this, args, expression; + recursionFn = recursionFn || noop; + if (!skipWatchIdCheck && isDefined(ast.watchId)) { + intoId = intoId || this.nextId(); + this.if_('i', + this.lazyAssign(intoId, this.computedMember('i', ast.watchId)), + this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true) + ); + return; + } + switch (ast.type) { + case AST.Program: + forEach(ast.body, function(expression, pos) { + self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; }); + if (pos !== ast.body.length - 1) { + self.current().body.push(right, ';'); + } else { + self.return_(right); + } + }); + break; + case AST.Literal: + expression = this.escape(ast.value); + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.UnaryExpression: + this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); + expression = ast.operator + '(' + this.ifDefined(right, 0) + ')'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.BinaryExpression: + this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; }); + this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; }); + if (ast.operator === '+') { + expression = this.plus(left, right); + } else if (ast.operator === '-') { + expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0); + } else { + expression = '(' + left + ')' + ast.operator + '(' + right + ')'; + } + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.LogicalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.left, intoId); + self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId)); + recursionFn(intoId); + break; + case AST.ConditionalExpression: + intoId = intoId || this.nextId(); + self.recurse(ast.test, intoId); + self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId)); + recursionFn(intoId); + break; + case AST.Identifier: + intoId = intoId || this.nextId(); + if (nameId) { + nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s'); + nameId.computed = false; + nameId.name = ast.name; + } + ensureSafeMemberName(ast.name); + self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), + function() { + self.if_(self.stage === 'inputs' || 's', function() { + if (create && create !== 1) { + self.if_( + self.not(self.nonComputedMember('s', ast.name)), + self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); + } + self.assign(intoId, self.nonComputedMember('s', ast.name)); + }); + }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) + ); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { + self.addEnsureSafeObject(intoId); + } + recursionFn(intoId); + break; + case AST.MemberExpression: + left = nameId && (nameId.context = this.nextId()) || this.nextId(); + intoId = intoId || this.nextId(); + self.recurse(ast.object, left, undefined, function() { + self.if_(self.notNull(left), function() { + if (create && create !== 1) { + self.addEnsureSafeAssignContext(left); + } + if (ast.computed) { + right = self.nextId(); + self.recurse(ast.property, right); + self.getStringValue(right); + self.addEnsureSafeMemberName(right); + if (create && create !== 1) { + self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); + } + expression = self.ensureSafeObject(self.computedMember(left, right)); + self.assign(intoId, expression); + if (nameId) { + nameId.computed = true; + nameId.name = right; + } + } else { + ensureSafeMemberName(ast.property.name); + if (create && create !== 1) { + self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + } + expression = self.nonComputedMember(left, ast.property.name); + if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { + expression = self.ensureSafeObject(expression); + } + self.assign(intoId, expression); + if (nameId) { + nameId.computed = false; + nameId.name = ast.property.name; + } + } + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }, !!create); + break; + case AST.CallExpression: + intoId = intoId || this.nextId(); + if (ast.filter) { + right = self.filter(ast.callee.name); + args = []; + forEach(ast.arguments, function(expr) { + var argument = self.nextId(); + self.recurse(expr, argument); + args.push(argument); + }); + expression = right + '(' + args.join(',') + ')'; + self.assign(intoId, expression); + recursionFn(intoId); + } else { + right = self.nextId(); + left = {}; + args = []; + self.recurse(ast.callee, right, left, function() { + self.if_(self.notNull(right), function() { + self.addEnsureSafeFunction(right); + forEach(ast.arguments, function(expr) { + self.recurse(expr, self.nextId(), undefined, function(argument) { + args.push(self.ensureSafeObject(argument)); + }); + }); + if (left.name) { + if (!self.state.expensiveChecks) { + self.addEnsureSafeObject(left.context); + } + expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; + } else { + expression = right + '(' + args.join(',') + ')'; + } + expression = self.ensureSafeObject(expression); + self.assign(intoId, expression); + }, function() { + self.assign(intoId, 'undefined'); + }); + recursionFn(intoId); + }); + } + break; + case AST.AssignmentExpression: + right = this.nextId(); + left = {}; + if (!isAssignable(ast.left)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + this.recurse(ast.left, undefined, left, function() { + self.if_(self.notNull(left.context), function() { + self.recurse(ast.right, right); + self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); + self.addEnsureSafeAssignContext(left.context); + expression = self.member(left.context, left.name, left.computed) + ast.operator + right; + self.assign(intoId, expression); + recursionFn(intoId || expression); + }); + }, 1); + break; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + self.recurse(expr, self.nextId(), undefined, function(argument) { + args.push(argument); + }); + }); + expression = '[' + args.join(',') + ']'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.ObjectExpression: + args = []; + forEach(ast.properties, function(property) { + self.recurse(property.value, self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + recursionFn(expression); + break; + case AST.ThisExpression: + this.assign(intoId, 's'); + recursionFn('s'); + break; + case AST.NGValueParameter: + this.assign(intoId, 'v'); + recursionFn('v'); + break; + } + }, + + getHasOwnProperty: function(element, property) { + var key = element + '.' + property; + var own = this.current().own; + if (!own.hasOwnProperty(key)) { + own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')'); + } + return own[key]; + }, + + assign: function(id, value) { + if (!id) return; + this.current().body.push(id, '=', value, ';'); + return id; + }, - // http://jsperf.com/angularjs-parse-getter/6 - if (options.csp) { - if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks); + filter: function(filterName) { + if (!this.state.filters.hasOwnProperty(filterName)) { + this.state.filters[filterName] = this.nextId(true); + } + return this.state.filters[filterName]; + }, + + ifDefined: function(id, defaultValue) { + return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')'; + }, + + plus: function(left, right) { + return 'plus(' + left + ',' + right + ')'; + }, + + return_: function(id) { + this.current().body.push('return ', id, ';'); + }, + + if_: function(test, alternate, consequent) { + if (test === true) { + alternate(); } else { - fn = function cspSafeGetter(scope, locals) { - var i = 0, val; - do { - val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, expensiveChecks)(scope, locals); + var body = this.current().body; + body.push('if(', test, '){'); + alternate(); + body.push('}'); + if (consequent) { + body.push('else{'); + consequent(); + body.push('}'); + } + } + }, - locals = undefined; // clear after first iteration - scope = val; - } while (i < pathKeysLength); - return val; - }; + not: function(expression) { + return '!(' + expression + ')'; + }, + + notNull: function(expression) { + return expression + '!=null'; + }, + + nonComputedMember: function(left, right) { + return left + '.' + right; + }, + + computedMember: function(left, right) { + return left + '[' + right + ']'; + }, + + member: function(left, right, computed) { + if (computed) return this.computedMember(left, right); + return this.nonComputedMember(left, right); + }, + + addEnsureSafeObject: function(item) { + this.current().body.push(this.ensureSafeObject(item), ';'); + }, + + addEnsureSafeMemberName: function(item) { + this.current().body.push(this.ensureSafeMemberName(item), ';'); + }, + + addEnsureSafeFunction: function(item) { + this.current().body.push(this.ensureSafeFunction(item), ';'); + }, + + addEnsureSafeAssignContext: function(item) { + this.current().body.push(this.ensureSafeAssignContext(item), ';'); + }, + + ensureSafeObject: function(item) { + return 'ensureSafeObject(' + item + ',text)'; + }, + + ensureSafeMemberName: function(item) { + return 'ensureSafeMemberName(' + item + ',text)'; + }, + + ensureSafeFunction: function(item) { + return 'ensureSafeFunction(' + item + ',text)'; + }, + + getStringValue: function(item) { + this.assign(item, 'getStringValue(' + item + ',text)'); + }, + + ensureSafeAssignContext: function(item) { + return 'ensureSafeAssignContext(' + item + ',text)'; + }, + + lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { + var self = this; + return function() { + self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck); + }; + }, + + lazyAssign: function(id, value) { + var self = this; + return function() { + self.assign(id, value); + }; + }, + + stringEscapeRegex: /[^ a-zA-Z0-9]/g, + + stringEscapeFn: function(c) { + return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); + }, + + escape: function(value) { + if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + if (isNumber(value)) return value.toString(); + if (value === true) return 'true'; + if (value === false) return 'false'; + if (value === null) return 'null'; + if (typeof value === 'undefined') return 'undefined'; + + throw $parseMinErr('esc', 'IMPOSSIBLE'); + }, + + nextId: function(skip, init) { + var id = 'v' + (this.state.nextId++); + if (!skip) { + this.current().vars.push(id + (init ? '=' + init : '')); } - } else { - var code = ''; - if (expensiveChecks) { - code += 's = eso(s, fe);\nl = eso(l, fe);\n'; - } - var needsEnsureSafeObject = expensiveChecks; - forEach(pathKeys, function(key, index) { - ensureSafeMemberName(key, fullExp); - var lookupJs = (index - // we simply dereference 's' on any .dot notation - ? 's' - // but if we are first then we check locals first, and if so read it first - : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key; - if (expensiveChecks || isPossiblyDangerousMemberName(key)) { - lookupJs = 'eso(' + lookupJs + ', fe)'; - needsEnsureSafeObject = true; - } - code += 'if(s == null) return undefined;\n' + - 's=' + lookupJs + ';\n'; + return id; + }, + + current: function() { + return this.state[this.state.computing]; + } +}; + + +function ASTInterpreter(astBuilder, $filter) { + this.astBuilder = astBuilder; + this.$filter = $filter; +} + +ASTInterpreter.prototype = { + compile: function(expression, expensiveChecks) { + var self = this; + var ast = this.astBuilder.ast(expression); + this.expression = expression; + this.expensiveChecks = expensiveChecks; + findConstantAndWatchExpressions(ast, self.$filter); + var assignable; + var assign; + if ((assignable = assignableAST(ast))) { + assign = this.recurse(assignable); + } + var toWatch = getInputs(ast.body); + var inputs; + if (toWatch) { + inputs = []; + forEach(toWatch, function(watch, key) { + var input = self.recurse(watch); + watch.input = input; + inputs.push(input); + watch.watchId = key; + }); + } + var expressions = []; + forEach(ast.body, function(expression) { + expressions.push(self.recurse(expression.expression)); }); - code += 'return s;'; + var fn = ast.body.length === 0 ? function() {} : + ast.body.length === 1 ? expressions[0] : + function(scope, locals) { + var lastValue; + forEach(expressions, function(exp) { + lastValue = exp(scope, locals); + }); + return lastValue; + }; + if (assign) { + fn.assign = function(scope, value, locals) { + return assign(scope, locals, value); + }; + } + if (inputs) { + fn.inputs = inputs; + } + fn.literal = isLiteral(ast); + fn.constant = isConstant(ast); + return fn; + }, - /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject - /* jshint +W054 */ - evaledFnGetter.toString = valueFn(code); - if (needsEnsureSafeObject) { - evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp); + recurse: function(ast, context, create) { + var left, right, self = this, args, expression; + if (ast.input) { + return this.inputs(ast.input, ast.watchId); + } + switch (ast.type) { + case AST.Literal: + return this.value(ast.value, context); + case AST.UnaryExpression: + right = this.recurse(ast.argument); + return this['unary' + ast.operator](right, context); + case AST.BinaryExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.LogicalExpression: + left = this.recurse(ast.left); + right = this.recurse(ast.right); + return this['binary' + ast.operator](left, right, context); + case AST.ConditionalExpression: + return this['ternary?:']( + this.recurse(ast.test), + this.recurse(ast.alternate), + this.recurse(ast.consequent), + context + ); + case AST.Identifier: + ensureSafeMemberName(ast.name, self.expression); + return self.identifier(ast.name, + self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), + context, create, self.expression); + case AST.MemberExpression: + left = this.recurse(ast.object, false, !!create); + if (!ast.computed) { + ensureSafeMemberName(ast.property.name, self.expression); + right = ast.property.name; + } + if (ast.computed) right = this.recurse(ast.property); + return ast.computed ? + this.computedMember(left, right, context, create, self.expression) : + this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + case AST.CallExpression: + args = []; + forEach(ast.arguments, function(expr) { + args.push(self.recurse(expr)); + }); + if (ast.filter) right = this.$filter(ast.callee.name); + if (!ast.filter) right = this.recurse(ast.callee, true); + return ast.filter ? + function(scope, locals, assign, inputs) { + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(args[i](scope, locals, assign, inputs)); + } + var value = right.apply(undefined, values, inputs); + return context ? {context: undefined, name: undefined, value: value} : value; + } : + function(scope, locals, assign, inputs) { + var rhs = right(scope, locals, assign, inputs); + var value; + if (rhs.value != null) { + ensureSafeObject(rhs.context, self.expression); + ensureSafeFunction(rhs.value, self.expression); + var values = []; + for (var i = 0; i < args.length; ++i) { + values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + } + value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + } + return context ? {value: value} : value; + }; + case AST.AssignmentExpression: + left = this.recurse(ast.left, true, 1); + right = this.recurse(ast.right); + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + ensureSafeObject(lhs.value, self.expression); + ensureSafeAssignContext(lhs.context); + lhs.context[lhs.name] = rhs; + return context ? {value: rhs} : rhs; + }; + case AST.ArrayExpression: + args = []; + forEach(ast.elements, function(expr) { + args.push(self.recurse(expr)); + }); + return function(scope, locals, assign, inputs) { + var value = []; + for (var i = 0; i < args.length; ++i) { + value.push(args[i](scope, locals, assign, inputs)); + } + return context ? {value: value} : value; + }; + case AST.ObjectExpression: + args = []; + forEach(ast.properties, function(property) { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + value: self.recurse(property.value) + }); + }); + return function(scope, locals, assign, inputs) { + var value = {}; + for (var i = 0; i < args.length; ++i) { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } + return context ? {value: value} : value; + }; + case AST.ThisExpression: + return function(scope) { + return context ? {value: scope} : scope; + }; + case AST.NGValueParameter: + return function(scope, locals, assign, inputs) { + return context ? {value: assign} : assign; + }; } - fn = evaledFnGetter; + }, + + 'unary+': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = +arg; + } else { + arg = 0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary-': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = argument(scope, locals, assign, inputs); + if (isDefined(arg)) { + arg = -arg; + } else { + arg = 0; + } + return context ? {value: arg} : arg; + }; + }, + 'unary!': function(argument, context) { + return function(scope, locals, assign, inputs) { + var arg = !argument(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary+': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = plusFn(lhs, rhs); + return context ? {value: arg} : arg; + }; + }, + 'binary-': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs = right(scope, locals, assign, inputs); + var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0); + return context ? {value: arg} : arg; + }; + }, + 'binary*': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary/': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary%': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary===': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary==': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary!=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary<=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary>=': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary&&': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'binary||': function(left, right, context) { + return function(scope, locals, assign, inputs) { + var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + 'ternary?:': function(test, alternate, consequent, context) { + return function(scope, locals, assign, inputs) { + var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs); + return context ? {value: arg} : arg; + }; + }, + value: function(value, context) { + return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; + }, + identifier: function(name, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var base = locals && (name in locals) ? locals : scope; + if (create && create !== 1 && base && !(base[name])) { + base[name] = {}; + } + var value = base ? base[name] : undefined; + if (expensiveChecks) { + ensureSafeObject(value, expression); + } + if (context) { + return {context: base, name: name, value: value}; + } else { + return value; + } + }; + }, + computedMember: function(left, right, context, create, expression) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + var rhs; + var value; + if (lhs != null) { + rhs = right(scope, locals, assign, inputs); + rhs = getStringValue(rhs); + ensureSafeMemberName(rhs, expression); + if (create && create !== 1) { + ensureSafeAssignContext(lhs); + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } + } + value = lhs[rhs]; + ensureSafeObject(value, expression); + } + if (context) { + return {context: lhs, name: rhs, value: value}; + } else { + return value; + } + }; + }, + nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + return function(scope, locals, assign, inputs) { + var lhs = left(scope, locals, assign, inputs); + if (create && create !== 1) { + ensureSafeAssignContext(lhs); + if (lhs && !(lhs[right])) { + lhs[right] = {}; + } + } + var value = lhs != null ? lhs[right] : undefined; + if (expensiveChecks || isPossiblyDangerousMemberName(right)) { + ensureSafeObject(value, expression); + } + if (context) { + return {context: lhs, name: right, value: value}; + } else { + return value; + } + }; + }, + inputs: function(input, watchId) { + return function(scope, value, locals, inputs) { + if (inputs) return inputs[watchId]; + return input(scope, value, locals); + }; } +}; - fn.sharedGetter = true; - fn.assign = function(self, value, locals) { - return setter(self, locals, path, value, path); - }; - getterFnCache[path] = fn; - return fn; +/** + * @constructor + */ +var Parser = function(lexer, $filter, options) { + this.lexer = lexer; + this.$filter = $filter; + this.options = options; + this.ast = new AST(this.lexer); + this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : + new ASTCompiler(this.ast, $filter); +}; + +Parser.prototype = { + constructor: Parser, + + parse: function(text) { + return this.astCompiler.compile(text, this.options.expensiveChecks); + } +}; + +function isPossiblyDangerousMemberName(name) { + return name == 'constructor'; } var objectValueOf = Object.prototype.valueOf; @@ -12740,39 +14527,33 @@ function $ParseProvider() { var cacheDefault = createMap(); var cacheExpensive = createMap(); - - - this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + this.$get = ['$filter', function($filter) { + var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { - csp: $sniffer.csp, + csp: noUnsafeEval, expensiveChecks: false }, $parseOptionsExpensive = { - csp: $sniffer.csp, + csp: noUnsafeEval, expensiveChecks: true }; + var runningChecksEnabled = false; - function wrapSharedExpression(exp) { - var wrapped = exp; - - if (exp.sharedGetter) { - wrapped = function $parseWrapper(self, locals) { - return exp(self, locals); - }; - wrapped.literal = exp.literal; - wrapped.constant = exp.constant; - wrapped.assign = exp.assign; - } + $parse.$$runningExpensiveChecks = function() { + return runningChecksEnabled; + }; - return wrapped; - } + return $parse; - return function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn, expensiveChecks) { var parsedExpression, oneTime, cacheKey; + expensiveChecks = expensiveChecks || runningChecksEnabled; + switch (typeof exp) { case 'string': - cacheKey = exp = exp.trim(); + exp = exp.trim(); + cacheKey = exp; var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; @@ -12782,24 +14563,21 @@ function $ParseProvider() { oneTime = true; exp = exp.substring(2); } - var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions; var lexer = new Lexer(parseOptions); var parser = new Parser(lexer, $filter, parseOptions); parsedExpression = parser.parse(exp); - if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; } else if (oneTime) { - //oneTime is not part of the exp passed to the Parser so we may have to - //wrap the parsedExpression before adding a $$watchDelegate - parsedExpression = wrapSharedExpression(parsedExpression); parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; + oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; } else if (parsedExpression.inputs) { parsedExpression.$$watchDelegate = inputsWatchDelegate; } - + if (expensiveChecks) { + parsedExpression = expensiveChecksInterceptor(parsedExpression); + } cache[cacheKey] = parsedExpression; } return addInterceptor(parsedExpression, interceptorFn); @@ -12810,21 +14588,30 @@ function $ParseProvider() { default: return addInterceptor(noop, interceptorFn); } - }; + } - function collectExpressionInputs(inputs, list) { - for (var i = 0, ii = inputs.length; i < ii; i++) { - var input = inputs[i]; - if (!input.constant) { - if (input.inputs) { - collectExpressionInputs(input.inputs, list); - } else if (list.indexOf(input) === -1) { // TODO(perf) can we do better? - list.push(input); - } - } + function expensiveChecksInterceptor(fn) { + if (!fn) return fn; + expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate; + expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign); + expensiveCheckFn.constant = fn.constant; + expensiveCheckFn.literal = fn.literal; + for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) { + fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]); } + expensiveCheckFn.inputs = fn.inputs; - return list; + return expensiveCheckFn; + + function expensiveCheckFn(scope, locals, assign, inputs) { + var expensiveCheckOldValue = runningChecksEnabled; + runningChecksEnabled = true; + try { + return fn(scope, locals, assign, inputs); + } finally { + runningChecksEnabled = expensiveCheckOldValue; + } + } } function expressionInputDirtyCheck(newValue, oldValueOfValue) { @@ -12852,28 +14639,28 @@ function $ParseProvider() { return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } - function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var inputExpressions = parsedExpression.$$inputs || - (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); - + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var inputExpressions = parsedExpression.inputs; var lastResult; if (inputExpressions.length === 1) { - var oldInputValue = expressionInputDirtyCheck; // init to something unique so that equals check fails + var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) { - lastResult = parsedExpression(scope); - oldInputValue = newInputValue && getValueOf(newInputValue); + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); + oldInputValueOf = newInputValue && getValueOf(newInputValue); } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } var oldInputValueOfValues = []; + var oldInputValues = []; for (var i = 0, ii = inputExpressions.length; i < ii; i++) { oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + oldInputValues[i] = null; } return scope.$watch(function expressionInputsWatch(scope) { @@ -12882,16 +14669,17 @@ function $ParseProvider() { for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } } if (changed) { - lastResult = parsedExpression(scope); + lastResult = parsedExpression(scope, undefined, undefined, oldInputValues); } return lastResult; - }, listener, objectEquality); + }, listener, objectEquality, prettyPrintExpression); } function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { @@ -12953,16 +14741,17 @@ function $ParseProvider() { function addInterceptor(parsedExpression, interceptorFn) { if (!interceptorFn) return parsedExpression; var watchDelegate = parsedExpression.$$watchDelegate; + var useInputs = false; var regularWatch = watchDelegate !== oneTimeLiteralWatchDelegate && watchDelegate !== oneTimeWatchDelegate; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals) { - var value = parsedExpression(scope, locals); + var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals) { - var value = parsedExpression(scope, locals); + } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { + var value = parsedExpression(scope, locals, assign, inputs); var result = interceptorFn(value, scope, locals); // we only return the interceptor's result if the // initial value is defined (for bind-once) @@ -12977,7 +14766,8 @@ function $ParseProvider() { // If there is an interceptor, but no watchDelegate then treat the interceptor like // we treat filters - it is assumed to be a pure function unless flagged with $stateful fn.$$watchDelegate = inputsWatchDelegate; - fn.inputs = [parsedExpression]; + useInputs = !parsedExpression.inputs; + fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } return fn; @@ -13038,6 +14828,8 @@ function $ParseProvider() { * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * + * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise. + * * However, the more traditional CommonJS-style usage is still available, and documented below. * * [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an @@ -13125,9 +14917,11 @@ function $ParseProvider() { * provide a progress indication, before the promise is resolved or rejected. * * This method *returns a new promise* which is resolved or rejected via the return value of the - * `successCallback`, `errorCallback`. It also notifies via the return value of the - * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback - * method. + * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved + * with the value which is resolved in that promise using + * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). + * It also notifies via the return value of the `notifyCallback` method. The promise cannot be + * resolved or rejected from the notifyCallback method. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * @@ -13255,8 +15049,11 @@ function qFactory(nextTick, exceptionHandler) { this.$$state = { status: 0 }; } - Promise.prototype = { + extend(Promise.prototype, { then: function(onFulfilled, onRejected, progressBack) { + if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { + return this; + } var result = new Deferred(); this.$$state.pending = this.$$state.pending || []; @@ -13277,7 +15074,7 @@ function qFactory(nextTick, exceptionHandler) { return handleCallback(error, false, callback); }, progressBack); } - }; + }); //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native function simpleBind(context, fn) { @@ -13287,24 +15084,24 @@ function qFactory(nextTick, exceptionHandler) { } function processQueue(state) { - var fn, promise, pending; + var fn, deferred, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; for (var i = 0, ii = pending.length; i < ii; ++i) { - promise = pending[i][0]; + deferred = pending[i][0]; fn = pending[i][state.status]; try { if (isFunction(fn)) { - promise.resolve(fn(state.value)); + deferred.resolve(fn(state.value)); } else if (state.status === 1) { - promise.resolve(state.value); + deferred.resolve(state.value); } else { - promise.reject(state.value); + deferred.reject(state.value); } } catch (e) { - promise.reject(e); + deferred.reject(e); exceptionHandler(e); } } @@ -13324,7 +15121,7 @@ function qFactory(nextTick, exceptionHandler) { this.notify = simpleBind(this, this.notify); } - Deferred.prototype = { + extend(Deferred.prototype, { resolve: function(val) { if (this.promise.$$state.status) return; if (val === this.promise) { @@ -13387,7 +15184,7 @@ function qFactory(nextTick, exceptionHandler) { }); } } - }; + }); /** * @ngdoc method @@ -13470,6 +15267,9 @@ function qFactory(nextTick, exceptionHandler) { * the promise comes from a source that can't be trusted. * * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback * @returns {Promise} Returns a promise of the passed value or promise */ @@ -13482,6 +15282,22 @@ function qFactory(nextTick, exceptionHandler) { /** * @ngdoc method + * @name $q#resolve + * @kind function + * + * @description + * Alias of {@link ng.$q#when when} to maintain naming consistency with ES6. + * + * @param {*} value Value or a promise + * @param {Function=} successCallback + * @param {Function=} errorCallback + * @param {Function=} progressCallback + * @returns {Promise} Returns a promise of the passed value or promise + */ + var resolve = when; + + /** + * @ngdoc method * @name $q#all * @kind function * @@ -13548,6 +15364,7 @@ function qFactory(nextTick, exceptionHandler) { $Q.defer = defer; $Q.reject = reject; $Q.when = when; + $Q.resolve = resolve; $Q.all = all; return $Q; @@ -13563,7 +15380,7 @@ function $$RAFProvider() { //rAF $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; - var rafFn = rafSupported + var raf = rafSupported ? function(fn) { var id = requestAnimationFrame(fn); return function() { @@ -13577,47 +15394,9 @@ function $$RAFProvider() { //rAF }; }; - queueFn.supported = rafSupported; - - var cancelLastRAF; - var taskCount = 0; - var taskQueue = []; - return queueFn; + raf.supported = rafSupported; - function flush() { - for (var i = 0; i < taskQueue.length; i++) { - var task = taskQueue[i]; - if (task) { - taskQueue[i] = null; - task(); - } - } - taskCount = taskQueue.length = 0; - } - - function queueFn(asyncFn) { - var index = taskQueue.length; - - taskCount++; - taskQueue.push(asyncFn); - - if (index === 0) { - cancelLastRAF = rafFn(flush); - } - - return function cancelQueueFn() { - if (index >= 0) { - taskQueue[index] = null; - index = null; - - if (--taskCount === 0 && cancelLastRAF) { - cancelLastRAF(); - cancelLastRAF = null; - taskQueue.length = 0; - } - } - }; - } + return raf; }]; } @@ -13635,15 +15414,15 @@ function $$RAFProvider() { //rAF * exposed as $$____ properties * * Loop operations are optimized by using while(count--) { ... } - * - this means that in order to keep the same order of execution as addition we have to add + * - This means that in order to keep the same order of execution as addition we have to add * items to the array at the beginning (unshift) instead of at the end (push) * * Child scopes are created and removed often - * - Using an array would be slow since inserts in middle are expensive so we use linked list + * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists * - * There are few watches then a lot of observers. This is why you don't want the observer to be - * implemented in the same way as watch. Watch requires return of initialization function which - * are expensive to construct. + * There are fewer watches than observers. This is why you don't want the observer to be implemented + * in the same way as watch. Watch requires return of the initialization function which is expensive + * to construct. */ @@ -13685,7 +15464,7 @@ function $$RAFProvider() { //rAF * Every application has a single root {@link ng.$rootScope.Scope scope}. * All other scopes are descendant scopes of the root scope. Scopes provide separation * between the model and the view, via a mechanism for watching the model for changes. - * They also provide an event emission/broadcast and subscription facility. See the + * They also provide event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider() { @@ -13707,6 +15486,7 @@ function $RootScopeProvider() { this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; + this.$$watchersCount = 0; this.$id = nextUid(); this.$$ChildScope = null; } @@ -13721,6 +15501,29 @@ function $RootScopeProvider() { $event.currentScope.$$destroyed = true; } + function cleanUpScope($scope) { + + if (msie === 9) { + // There is a memory leak in IE9 if all child scopes are not disconnected + // completely when a scope is destroyed. So this code will recurse up through + // all this scopes children + // + // See issue https://github.com/angular/angular.js/issues/10706 + $scope.$$childHead && cleanUpScope($scope.$$childHead); + $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling); + } + + // The code below works around IE9 and V8's memory leaks + // + // See: + // - https://code.google.com/p/v8/issues/detail?id=2073#c26 + // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 + // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 + + $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead = + $scope.$$childTail = $scope.$root = $scope.$$watchers = null; + } + /** * @ngdoc type * @name $rootScope.Scope @@ -13770,6 +15573,7 @@ function $RootScopeProvider() { this.$$destroyed = false; this.$$listeners = {}; this.$$listenerCount = {}; + this.$$watchersCount = 0; this.$$isolateBindings = null; } @@ -13872,10 +15676,10 @@ function $RootScopeProvider() { * Registers a `listener` callback to be executed whenever the `watchExpression` changes. * * - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest - * $digest()} and should return the value that will be watched. (Since - * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the - * `watchExpression` can execute multiple times per - * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) + * $digest()} and should return the value that will be watched. (`watchExpression` should not change + * its value when executed multiple times with the same input because it may be executed multiple + * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be + * [idempotent](http://en.wikipedia.org/wiki/Idempotence). * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, @@ -13979,11 +15783,11 @@ function $RootScopeProvider() { * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ - $watch: function(watchExp, listener, objectEquality) { + $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) { var get = $parse(watchExp); if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get); + return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, @@ -13991,7 +15795,7 @@ function $RootScopeProvider() { fn: listener, last: initWatchVal, get: get, - exp: watchExp, + exp: prettyPrintExpression || watchExp, eq: !!objectEquality }; @@ -14007,9 +15811,12 @@ function $RootScopeProvider() { // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + incrementWatchersCount(this, 1); return function deregisterWatch() { - arrayRemove(array, watcher); + if (arrayRemove(array, watcher) >= 0) { + incrementWatchersCount(scope, -1); + } lastDirtyWatch = null; }; }, @@ -14221,7 +16028,7 @@ function $RootScopeProvider() { // copy the items to oldValue and look for changes. newLength = 0; for (key in newValue) { - if (newValue.hasOwnProperty(key)) { + if (hasOwnProperty.call(newValue, key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; @@ -14243,7 +16050,7 @@ function $RootScopeProvider() { // we used to have more keys, need to find them and destroy them. changeDetected++; for (key in oldValue) { - if (!newValue.hasOwnProperty(key)) { + if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } @@ -14337,7 +16144,7 @@ function $RootScopeProvider() { * */ $digest: function() { - var watch, value, last, + var watch, value, last, fn, get, watchers, length, dirty, ttl = TTL, @@ -14383,7 +16190,8 @@ function $RootScopeProvider() { // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && + get = watch.get; + if ((value = get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' @@ -14391,7 +16199,8 @@ function $RootScopeProvider() { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); + fn = watch.fn; + fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; @@ -14417,7 +16226,7 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast - if (!(next = (current.$$childHead || + if (!(next = ((current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; @@ -14484,22 +16293,27 @@ function $RootScopeProvider() { * clean up DOM bindings before an element is removed from the DOM. */ $destroy: function() { - // we can't destroy the root scope or a scope that has been already destroyed + // We can't destroy a scope that has been already destroyed. if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; - if (this === $rootScope) return; + if (this === $rootScope) { + //Remove handlers attached to window when $rootScope is removed + $browser.$$applicationDestroyed(); + } + + incrementWatchersCount(this, -this.$$watchersCount); for (var eventName in this.$$listenerCount) { decrementListenerCount(this, this.$$listenerCount[eventName], eventName); } // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) - if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; + if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; @@ -14508,16 +16322,9 @@ function $RootScopeProvider() { this.$on = this.$watch = this.$watchGroup = function() { return noop; }; this.$$listeners = {}; - // All of the code below is bogus code that works around V8's memory leak via optimized code - // and inline caches. - // - // see: - // - https://code.google.com/p/v8/issues/detail?id=2073#c26 - // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 - // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 - - this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = this.$$watchers = null; + // Disconnect the next sibling to prevent `cleanUpScope` destroying those too + this.$$nextSibling = null; + cleanUpScope(this); }, /** @@ -14593,7 +16400,7 @@ function $RootScopeProvider() { }); } - asyncQueue.push({scope: this, expression: expr, locals: locals}); + asyncQueue.push({scope: this, expression: $parse(expr), locals: locals}); }, $$postDigest: function(fn) { @@ -14648,11 +16455,14 @@ function $RootScopeProvider() { $apply: function(expr) { try { beginPhase('$apply'); - return this.$eval(expr); + try { + return this.$eval(expr); + } finally { + clearPhase(); + } } catch (e) { $exceptionHandler(e); } finally { - clearPhase(); try { $rootScope.$digest(); } catch (e) { @@ -14682,6 +16492,7 @@ function $RootScopeProvider() { $applyAsync: function(expr) { var scope = this; expr && applyAsyncQueue.push($applyAsyncExpression); + expr = $parse(expr); scheduleApplyAsync(); function $applyAsyncExpression() { @@ -14913,6 +16724,11 @@ function $RootScopeProvider() { $rootScope.$$phase = null; } + function incrementWatchersCount(current, count) { + do { + current.$$watchersCount += count; + } while ((current = current.$parent)); + } function decrementListenerCount(current, count, name) { do { @@ -14952,6 +16768,21 @@ function $RootScopeProvider() { } /** + * @ngdoc service + * @name $rootElement + * + * @description + * The root element of Angular application. This is either the element where {@link + * ng.directive:ngApp ngApp} was declared or the element passed into + * {@link angular.bootstrap}. The element represents the root element of application. It is also the + * location where the application's {@link auto.$injector $injector} service gets + * published, and can be retrieved using `$rootElement.injector()`. + */ + + +// the implementation is in angular.bootstrap + +/** * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ @@ -15165,13 +16996,15 @@ function $SceDelegateProvider() { * @kind function * * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. * - * Note: **an empty whitelist array will block all URLs**! + * <div class="alert alert-warning"> + * **Note:** an empty whitelist array will block all URLs! + * </div> * * @return {Array} the currently set whitelist array. * @@ -15194,17 +17027,17 @@ function $SceDelegateProvider() { * @kind function * * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. + * provided. This must be an array or null. A snapshot of this array is used so further + * changes to the array are ignored. * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array. * - * The typical usage for the blacklist is to **block - * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as - * these would otherwise be trusted but actually return content from the redirected domain. + * The typical usage for the blacklist is to **block + * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as + * these would otherwise be trusted but actually return content from the redirected domain. * - * Finally, **the blacklist overrides the whitelist** and has the final say. + * Finally, **the blacklist overrides the whitelist** and has the final say. * * @return {Array} the currently set blacklist array. * @@ -15315,7 +17148,7 @@ function $SceDelegateProvider() { 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}', type, trustedValue); } - if (trustedValue === null || trustedValue === undefined || trustedValue === '') { + if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') { return trustedValue; } // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting @@ -15370,7 +17203,7 @@ function $SceDelegateProvider() { * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. */ function getTrusted(type, maybeTrusted) { - if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') { + if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -15445,7 +17278,7 @@ function $SceDelegateProvider() { * Here's an example of a binding in a privileged context: * * ``` - * <input ng-model="userHtml"> + * <input ng-model="userHtml" aria-label="User input"> * <div ng-bind-html="userHtml"></div> * ``` * @@ -15505,7 +17338,7 @@ function $SceDelegateProvider() { * By default, Angular only loads templates from the same domain and protocol as the application * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist + * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. * * *Please note*: @@ -15563,7 +17396,7 @@ function $SceDelegateProvider() { * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters * match themselves. * - `*`: matches zero or more occurrences of any character other than one of the following 6 - * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use + * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use * in a whitelist. * - `**`: matches zero or more occurrences of *any* character. As such, it's not * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. @@ -15829,7 +17662,7 @@ function $SceProvider() { * escaping. * * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resource_url, html, js and css. + * resourceUrl, html, js and css. * @param {*} value The value that that should be considered trusted/safe. * @returns {*} A value that can be used to stand in for the provided `value` in places * where Angular expects a $sce.trustAs() return value. @@ -16098,7 +17931,7 @@ function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, android = - int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), + toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, vendorPrefix, @@ -16125,8 +17958,8 @@ function $SnifferProvider() { animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); if (android && (!transitions || !animations)) { - transitions = isString(document.body.style.webkitTransition); - animations = isString(document.body.style.webkitAnimation); + transitions = isString(bodyStyle.webkitTransition); + animations = isString(bodyStyle.webkitAnimation); } } @@ -16184,7 +18017,7 @@ var $compileMinErr = minErr('$compile'); * @param {string|TrustedResourceUrl} tpl The HTTP request template URL * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty * - * @return {Promise} the HTTP Promise for the given. + * @return {Promise} a promise for the HTTP response data of the given URL. * * @property {number} totalPendingRequests total amount of pending template requests being downloaded. */ @@ -16222,12 +18055,14 @@ function $TemplateRequestProvider() { handleRequestFn.totalPendingRequests--; }) .then(function(response) { + $templateCache.put(tpl, response.data); return response.data; }, handleError); function handleError(resp) { if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); + throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); } return $q.reject(resp); } @@ -16357,6 +18192,7 @@ function $$TestabilityProvider() { function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { + var deferreds = {}; @@ -16369,31 +18205,42 @@ function $TimeoutProvider() { * block and delegates any exceptions to * {@link ng.$exceptionHandler $exceptionHandler} service. * - * The return value of registering a timeout function is a promise, which will be resolved when - * the timeout is reached and the timeout function is executed. + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. * * To cancel a timeout request, call `$timeout.cancel(promise)`. * * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to * synchronously flush the queue of deferred functions. * - * @param {function()} fn A function, whose execution should be delayed. + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. * @param {number=} [delay=0] Delay in milliseconds. * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { - var skipApply = (isDefined(invokeApply) && !invokeApply), + if (!isFunction(fn)) { + invokeApply = delay; + delay = fn; + fn = noop; + } + + var args = sliceArgs(arguments, 3), + skipApply = (isDefined(invokeApply) && !invokeApply), deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, timeoutId; timeoutId = $browser.defer(function() { try { - deferred.resolve(fn()); + deferred.resolve(fn.apply(null, args)); } catch (e) { deferred.reject(e); $exceptionHandler(e); @@ -16561,7 +18408,7 @@ function urlIsSameOrigin(requestUrl) { }]); </script> <div ng-controller="ExampleController"> - <input type="text" ng-model="greeting" /> + <input type="text" ng-model="greeting" aria-label="greeting" /> <button ng-click="doGreeting(greeting)">ALERT</button> </div> </file> @@ -16578,6 +18425,61 @@ function $WindowProvider() { this.$get = valueFn(window); } +/** + * @name $$cookieReader + * @requires $document + * + * @description + * This is a private service for reading cookies used by $http and ngCookies + * + * @return {Object} a key/value map of the current cookies + */ +function $$CookieReader($document) { + var rawDocument = $document[0] || {}; + var lastCookies = {}; + var lastCookieString = ''; + + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + + return function() { + var cookieArray, cookie, i, index, name; + var currentCookieString = rawDocument.cookie || ''; + + if (currentCookieString !== lastCookieString) { + lastCookieString = currentCookieString; + cookieArray = lastCookieString.split('; '); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + name = safeDecodeURIComponent(cookie.substring(0, index)); + // the first value that is seen for a cookie is the most + // specific one. values for the same cookie name that + // follow are for less specific paths. + if (isUndefined(lastCookies[name])) { + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); + } + } + } + } + return lastCookies; + }; +} + +$$CookieReader.$inject = ['$document']; + +function $$CookieReaderProvider() { + this.$get = $$CookieReader; +} + /* global currencyFilter: true, dateFilter: true, filterFilter: true, @@ -16693,6 +18595,7 @@ function $FilterProvider($provide) { * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores * (`myapp_subsection_filterx`). * </div> + * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ @@ -16774,9 +18677,11 @@ function $FilterProvider($provide) { * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but * **will** be matched by `{$: 'John'}`. * - * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The - * function is called for each element of `array`. The final result is an array of those - * elements that the predicate returned true for. + * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters. + * The function is called for each element of the array, with the element, its index, and + * the entire array itself as arguments. + * + * The final result is an array of those elements that the predicate returned true for. * * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in * determining if the expected value (from the filter expression) and actual value (from @@ -16794,6 +18699,9 @@ function $FilterProvider($provide) { * - `false|undefined`: A short hand for a function which will look for a substring match in case * insensitive way. * + * Primitive values are converted to strings. Objects are not compared against primitives, + * unless they have a custom `toString` method (e.g. `Date` objects). + * * @example <example> <file name="index.html"> @@ -16804,7 +18712,7 @@ function $FilterProvider($provide) { {name:'Julie', phone:'555-8765'}, {name:'Juliette', phone:'555-5678'}]"></div> - Search: <input ng-model="searchText"> + <label>Search: <input ng-model="searchText"></label> <table id="searchTextResults"> <tr><th>Name</th><th>Phone</th></tr> <tr ng-repeat="friend in friends | filter:searchText"> @@ -16813,10 +18721,10 @@ function $FilterProvider($provide) { </tr> </table> <hr> - Any: <input ng-model="search.$"> <br> - Name only <input ng-model="search.name"><br> - Phone only <input ng-model="search.phone"><br> - Equality <input type="checkbox" ng-model="strict"><br> + <label>Any: <input ng-model="search.$"></label> <br> + <label>Name only <input ng-model="search.name"></label><br> + <label>Phone only <input ng-model="search.phone"></label><br> + <label>Equality <input type="checkbox" ng-model="strict"></label><br> <table id="searchObjResults"> <tr><th>Name</th><th>Phone</th></tr> <tr ng-repeat="friendObj in friends | filter:search:strict"> @@ -16864,9 +18772,15 @@ function $FilterProvider($provide) { */ function filterFilter() { return function(array, expression, comparator) { - if (!isArray(array)) return array; + if (!isArrayLike(array)) { + if (array == null) { + return array; + } else { + throw minErr('filter')('notarray', 'Expected array but received: {0}', array); + } + } - var expressionType = (expression !== null) ? typeof expression : 'null'; + var expressionType = getTypeForFilter(expression); var predicateFn; var matchAgainstAnyProp; @@ -16888,7 +18802,7 @@ function filterFilter() { return array; } - return array.filter(predicateFn); + return Array.prototype.filter.call(array, predicateFn); }; } @@ -16909,8 +18823,8 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) { // No substring matching against `null`; only match against `null` return actual === expected; } - if (isObject(actual) || isObject(expected)) { - // Prevent an object to be considered equal to a string like `'[object'` + if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) { + // Should not compare primitives against objects, unless they have custom `toString` method return false; } @@ -16931,8 +18845,8 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) { } function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { - var actualType = (actual !== null) ? typeof actual : 'null'; - var expectedType = (expected !== null) ? typeof expected : 'null'; + var actualType = getTypeForFilter(actual); + var expectedType = getTypeForFilter(expected); if ((expectedType === 'string') && (expected.charAt(0) === '!')) { return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); @@ -16979,6 +18893,15 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc } } +// Used for easily differentiating between `null` and actual `object` +function getTypeForFilter(val) { + return (val === null) ? 'null' : typeof val; +} + +var MAX_DIGITS = 22; +var DECIMAL_SEP = '.'; +var ZERO_CHAR = '0'; + /** * @ngdoc filter * @name currency @@ -17004,7 +18927,7 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc }]); </script> <div ng-controller="ExampleController"> - <input type="number" ng-model="amount"> <br> + <input type="number" ng-model="amount" aria-label="amount"> <br> default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span> no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span> @@ -17024,9 +18947,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc } element(by.model('amount')).clear(); element(by.model('amount')).sendKeys('-1234'); - expect(element(by.id('currency-default')).getText()).toBe('($1,234.00)'); - expect(element(by.id('currency-custom')).getText()).toBe('(USD$1,234.00)'); - expect(element(by.id('currency-no-fractions')).getText()).toBe('(USD$1,234)'); + expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00'); + expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234'); }); </file> </example> @@ -17060,14 +18983,15 @@ function currencyFilter($locale) { * Formats a number as text. * * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. * If the input is not a number an empty string is returned. * + * * @param {number|string} number Number to format. * @param {(number|string)=} fractionSize Number of decimal places to round the number to. * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * @returns {string} Number rounded to fractionSize and places a “,” after each third digit. * * @example <example module="numberFilterExample"> @@ -17079,7 +19003,7 @@ function currencyFilter($locale) { }]); </script> <div ng-controller="ExampleController"> - Enter number: <input ng-model='val'><br> + <label>Enter number: <input ng-model='val'></label><br> Default formatting: <span id='number-default'>{{val | number}}</span><br> No fractions: <span>{{val | number:0}}</span><br> Negative number: <span>{{-val | number:4}}</span> @@ -17102,8 +19026,6 @@ function currencyFilter($locale) { </file> </example> */ - - numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; @@ -17117,86 +19039,194 @@ function numberFilter($locale) { }; } -var DECIMAL_SEP = '.'; -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (!isFinite(number) || isObject(number)) return ''; - - var isNegative = number < 0; - number = Math.abs(number); - var numStr = number + '', - formatedText = '', - parts = []; - - var hasExponent = false; - if (numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - number = 0; - } else { - formatedText = numStr; - hasExponent = true; +/** + * Parse a number (as a string) into three components that can be used + * for formatting the number. + * + * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) + * + * @param {string} numStr The number to parse + * @return {object} An object describing this number, containing the following keys: + * - d : an array of digits containing leading zeros as necessary + * - i : the number of the digits in `d` that are to the left of the decimal point + * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` + * + */ +function parse(numStr) { + var exponent = 0, digits, numberOfIntegerDigits; + var i, j, zeros; + + // Decimal point? + if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); + } + + // Exponential form? + if ((i = numStr.search(/e/i)) > 0) { + // Work out the exponent. + if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; + numberOfIntegerDigits += +numStr.slice(i + 1); + numStr = numStr.substring(0, i); + } else if (numberOfIntegerDigits < 0) { + // There was no decimal point or exponent so it is an integer. + numberOfIntegerDigits = numStr.length; + } + + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */} + + if (i == (zeros = numStr.length)) { + // The digits are all zero. + digits = [0]; + numberOfIntegerDigits = 1; + } else { + // Count the number of trailing zeros + zeros--; + while (numStr.charAt(zeros) == ZERO_CHAR) zeros--; + + // Trailing zeros are insignificant so ignore them + numberOfIntegerDigits -= i; + digits = []; + // Convert string to array of digits without leading/trailing zeros. + for (j = 0; i <= zeros; i++, j++) { + digits[j] = +numStr.charAt(i); } } - if (!hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + // If the number overflows the maximum allowed digits then use an exponent. + if (numberOfIntegerDigits > MAX_DIGITS) { + digits = digits.splice(0, MAX_DIGITS - 1); + exponent = numberOfIntegerDigits - 1; + numberOfIntegerDigits = 1; + } - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); + return { d: digits, e: exponent, i: numberOfIntegerDigits }; +} + +/** + * Round the parsed number to the specified number of decimal places + * This function changed the parsedNumber in-place + */ +function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { + var digits = parsedNumber.d; + var fractionLen = digits.length - parsedNumber.i; + + // determine fractionSize if it is not specified; `+fractionSize` converts it to a number + fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; + + // The index of the digit to where rounding is to occur + var roundAt = fractionSize + parsedNumber.i; + var digit = digits[roundAt]; + + if (roundAt > 0) { + digits.splice(roundAt); + } else { + // We rounded to zero so reset the parsedNumber + parsedNumber.i = 1; + digits.length = roundAt = fractionSize + 1; + for (var i=0; i < roundAt; i++) digits[i] = 0; } - // safely round numbers in JS without hitting imprecisions of floating-point arithmetics - // inspired by: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round - number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + if (digit >= 5) digits[roundAt - 1]++; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; + // Pad out with zeros to get the required fraction length + for (; fractionLen < fractionSize; fractionLen++) digits.push(0); - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i) % group === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } + // Do any carrying, e.g. a digit was rounded up to 10 + var carry = digits.reduceRight(function(carry, d, i, digits) { + d = d + carry; + digits[i] = d % 10; + return Math.floor(d / 10); + }, 0); + if (carry) { + digits.unshift(carry); + parsedNumber.i++; } +} - for (i = pos; i < whole.length; i++) { - if ((whole.length - i) % lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); +/** + * Format a number into a string + * @param {number} number The number to format + * @param {{ + * minFrac, // the minimum number of digits required in the fraction part of the number + * maxFrac, // the maximum number of digits required in the fraction part of the number + * gSize, // number of digits in each group of separated digits + * lgSize, // number of digits in the last group of digits before the decimal separator + * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) + * posPre, // the string to go in front of a positive number + * negSuf, // the string to go after a negative number (e.g. `)`) + * posSuf // the string to go after a positive number + * }} pattern + * @param {string} groupSep The string to separate groups of number (e.g. `,`) + * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) + * @param {[type]} fractionSize The size of the fractional part of the number + * @return {string} The number formatted as a string + */ +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + + if (!(isString(number) || isNumber(number)) || isNaN(number)) return ''; + + var isInfinity = !isFinite(number); + var isZero = false; + var numStr = Math.abs(number) + '', + formattedText = '', + parsedNumber; + + if (isInfinity) { + formattedText = '\u221e'; + } else { + parsedNumber = parse(numStr); + + roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); + + var digits = parsedNumber.d; + var integerLen = parsedNumber.i; + var exponent = parsedNumber.e; + var decimals = []; + isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true); + + // pad zeros for small numbers + while (integerLen < 0) { + digits.unshift(0); + integerLen++; } - // format fraction part. - while (fraction.length < fractionSize) { - fraction += '0'; + // extract decimals digits + if (integerLen > 0) { + decimals = digits.splice(integerLen); + } else { + decimals = digits; + digits = [0]; } - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); - } else { - if (fractionSize > 0 && number < 1) { - formatedText = number.toFixed(fractionSize); - number = parseFloat(formatedText); + // format the integer digits with grouping separators + var groups = []; + if (digits.length > pattern.lgSize) { + groups.unshift(digits.splice(-pattern.lgSize).join('')); } - } + while (digits.length > pattern.gSize) { + groups.unshift(digits.splice(-pattern.gSize).join('')); + } + if (digits.length) { + groups.unshift(digits.join('')); + } + formattedText = groups.join(groupSep); - if (number === 0) { - isNegative = false; - } + // append the decimal digits + if (decimals.length) { + formattedText += decimalSep + decimals.join(''); + } - parts.push(isNegative ? pattern.negPre : pattern.posPre, - formatedText, - isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); + if (exponent) { + formattedText += 'e+' + exponent; + } + } + if (number < 0 && !isZero) { + return pattern.negPre + formattedText + pattern.negSuf; + } else { + return pattern.posPre + formattedText + pattern.posSuf; + } } function padNumber(num, digits, trim) { @@ -17206,9 +19236,10 @@ function padNumber(num, digits, trim) { num = -num; } num = '' + num; - while (num.length < digits) num = '0' + num; - if (trim) + while (num.length < digits) num = ZERO_CHAR + num; + if (trim) { num = num.substr(num.length - digits); + } return neg + num; } @@ -17217,8 +19248,9 @@ function dateGetter(name, size, offset, trim) { offset = offset || 0; return function(date) { var value = date['get' + name](); - if (offset > 0 || value > -offset) + if (offset > 0 || value > -offset) { value += offset; + } if (value === 0 && offset == -12) value = 12; return padNumber(value, size, trim); }; @@ -17233,8 +19265,8 @@ function dateStrGetter(name, shortForm) { }; } -function timeZoneGetter(date) { - var zone = -1 * date.getTimezoneOffset(); +function timeZoneGetter(date, formats, offset) { + var zone = -1 * offset; var paddedZone = (zone >= 0) ? "+" : ""; paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + @@ -17378,7 +19410,9 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| * specified in the string input, the time is considered to be in the local timezone. * @param {string=} format Formatting rules (see Description). If not specified, * `mediumDate` is used. - * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported. + * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) * If not specified, the timezone of the browser will be used. * @returns {string} Formatted string or the input if input is not recognized as date/millis. * @@ -17424,13 +19458,13 @@ function dateFilter($locale) { timeSetter = match[8] ? date.setUTCHours : date.setHours; if (match[9]) { - tzHour = int(match[9] + match[10]); - tzMin = int(match[9] + match[11]); + tzHour = toInt(match[9] + match[10]); + tzMin = toInt(match[9] + match[11]); } - dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3])); - var h = int(match[4] || 0) - tzHour; - var m = int(match[5] || 0) - tzMin; - var s = int(match[6] || 0); + dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3])); + var h = toInt(match[4] || 0) - tzHour; + var m = toInt(match[5] || 0) - tzMin; + var s = toInt(match[6] || 0); var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; @@ -17447,14 +19481,14 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); + date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date); } if (isNumber(date)) { date = new Date(date); } - if (!isDate(date)) { + if (!isDate(date) || !isFinite(date.getTime())) { return date; } @@ -17469,14 +19503,15 @@ function dateFilter($locale) { } } - if (timezone && timezone === 'UTC') { - date = new Date(date.getTime()); - date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); + var dateTimezoneOffset = date.getTimezoneOffset(); + if (timezone) { + dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + date = convertTimezoneToLocal(date, timezone, true); } forEach(parts, function(value) { fn = DATE_FORMATS[value]; - text += fn ? fn(date, $locale.DATETIME_FORMATS) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) + : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); }); return text; @@ -17561,7 +19596,10 @@ var uppercaseFilter = valueFn(uppercase); * @param {string|number} limit The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string - * are copied. The `limit` will be trimmed if it exceeds `array.length` + * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, + * the input will be returned unchanged. + * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin` + * indicates an offset from the end of `input`. Defaults to `0`. * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array * had less than `limit` elements. * @@ -17580,11 +19618,20 @@ var uppercaseFilter = valueFn(uppercase); }]); </script> <div ng-controller="ExampleController"> - Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit"> + <label> + Limit {{numbers}} to: + <input type="number" step="1" ng-model="numLimit"> + </label> <p>Output numbers: {{ numbers | limitTo:numLimit }}</p> - Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit"> + <label> + Limit {{letters}} to: + <input type="number" step="1" ng-model="letterLimit"> + </label> <p>Output letters: {{ letters | limitTo:letterLimit }}</p> - Limit {{longNumber}} to: <input type="number" step="1" ng-model="longNumberLimit"> + <label> + Limit {{longNumber}} to: + <input type="number" step="1" ng-model="longNumberLimit"> + </label> <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p> </div> </file> @@ -17633,21 +19680,28 @@ var uppercaseFilter = valueFn(uppercase); </example> */ function limitToFilter() { - return function(input, limit) { - if (isNumber(input)) input = input.toString(); - if (!isArray(input) && !isString(input)) return input; - + return function(input, limit, begin) { if (Math.abs(Number(limit)) === Infinity) { limit = Number(limit); } else { - limit = int(limit); + limit = toInt(limit); } + if (isNaN(limit)) return input; + + if (isNumber(input)) input = input.toString(); + if (!isArray(input) && !isString(input)) return input; - //NaN check on limit - if (limit) { - return limit > 0 ? input.slice(0, limit) : input.slice(limit); + begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); + begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; + + if (limit >= 0) { + return input.slice(begin, begin + limit); } else { - return isString(input) ? "" : []; + if (begin === 0) { + return input.slice(limit, input.length); + } else { + return input.slice(Math.max(0, begin + limit), begin); + } } }; } @@ -17660,7 +19714,7 @@ function limitToFilter() { * @description * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically * for strings and numerically for numbers. Note: if you notice numbers are not being sorted - * correctly, make sure they are actually being saved as numbers and not strings. + * as expected, make sure they are actually being saved as numbers and not strings. * * @param {Array} array The array to sort. * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be @@ -17693,17 +19747,6 @@ function limitToFilter() { * `reverse` is not set, which means it defaults to `false`. <example module="orderByExample"> <file name="index.html"> - <script> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}]; - }]); - </script> <div ng-controller="ExampleController"> <table class="friend"> <tr> @@ -17719,6 +19762,17 @@ function limitToFilter() { </table> </div> </file> + <file name="script.js"> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}]; + }]); + </file> </example> * * The predicate and reverse parameters can be controlled dynamically through scope properties, @@ -17726,28 +19780,24 @@ function limitToFilter() { * @example <example module="orderByExample"> <file name="index.html"> - <script> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}]; - $scope.predicate = '-age'; - }]); - </script> <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> - [ <a href="" ng-click="predicate=''">unsorted</a> ] + <button ng-click="predicate=''">Set to unsorted</button> <table class="friend"> <tr> - <th><a href="" ng-click="predicate = 'name'; reverse=false">Name</a> - (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th> - <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> - <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></th> + <th> + <button ng-click="order('name')">Name</button> + <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <button ng-click="order('phone')">Phone Number</button> + <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <button ng-click="order('age')">Age</button> + <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> + </th> </tr> <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> <td>{{friend.name}}</td> @@ -17757,6 +19807,31 @@ function limitToFilter() { </table> </div> </file> + <file name="script.js"> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}]; + $scope.predicate = 'age'; + $scope.reverse = true; + $scope.order = function(predicate) { + $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; + $scope.predicate = predicate; + }; + }]); + </file> + <file name="style.css"> + .sortorder:after { + content: '\25b2'; + } + .sortorder.reverse:after { + content: '\25bc'; + } + </file> </example> * * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the @@ -17768,21 +19843,30 @@ function limitToFilter() { * @example <example module="orderByExample"> <file name="index.html"> - <div ng-controller="ExampleController"> - <table class="friend"> - <tr> - <th><a href="" ng-click="reverse=false;order('name', false)">Name</a> - (<a href="" ng-click="order('-name',false)">^</a>)</th> - <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th> - <th><a href="" ng-click="reverse=!reverse;order('age',reverse)">Age</a></th> - </tr> - <tr ng-repeat="friend in friends"> - <td>{{friend.name}}</td> - <td>{{friend.phone}}</td> - <td>{{friend.age}}</td> - </tr> - </table> - </div> + <div ng-controller="ExampleController"> + <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> + <table class="friend"> + <tr> + <th> + <button ng-click="order('name')">Name</button> + <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <button ng-click="order('phone')">Phone Number</button> + <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> + </th> + <th> + <button ng-click="order('age')">Age</button> + <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> + </th> + </tr> + <tr ng-repeat="friend in friends"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <td>{{friend.age}}</td> + </tr> + </table> + </div> </file> <file name="script.js"> @@ -17796,101 +19880,142 @@ function limitToFilter() { { name: 'Adam', phone: '555-5678', age: 35 }, { name: 'Julie', phone: '555-8765', age: 29 } ]; - $scope.order = function(predicate, reverse) { - $scope.friends = orderBy($scope.friends, predicate, reverse); + $scope.order = function(predicate) { + $scope.predicate = predicate; + $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; + $scope.friends = orderBy($scope.friends, predicate, $scope.reverse); }; - $scope.order('-age',false); + $scope.order('age', true); }]); </file> + + <file name="style.css"> + .sortorder:after { + content: '\25b2'; + } + .sortorder.reverse:after { + content: '\25bc'; + } + </file> </example> */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse) { return function(array, sortPredicate, reverseOrder) { + if (!(isArrayLike(array))) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; + + if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } if (sortPredicate.length === 0) { sortPredicate = ['+']; } - sortPredicate = sortPredicate.map(function(predicate) { - var descending = false, get = predicate || identity; - if (isString(predicate)) { + + var predicates = processPredicates(sortPredicate, reverseOrder); + // Add a predicate at the end that evaluates to the element index. This makes the + // sort stable as it works as a tie-breaker when all the input predicates cannot + // distinguish between two elements. + predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1}); + + // The next three lines are a version of a Swartzian Transform idiom from Perl + // (sometimes called the Decorate-Sort-Undecorate idiom) + // See https://en.wikipedia.org/wiki/Schwartzian_transform + var compareValues = Array.prototype.map.call(array, getComparisonObject); + compareValues.sort(doComparison); + array = compareValues.map(function(item) { return item.value; }); + + return array; + + function getComparisonObject(value, index) { + return { + value: value, + predicateValues: predicates.map(function(predicate) { + return getPredicateValue(predicate.get(value), index); + }) + }; + } + + function doComparison(v1, v2) { + var result = 0; + for (var index=0, length = predicates.length; index < length; ++index) { + result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending; + if (result) break; + } + return result; + } + }; + + function processPredicates(sortPredicate, reverseOrder) { + reverseOrder = reverseOrder ? -1 : 1; + return sortPredicate.map(function(predicate) { + var descending = 1, get = identity; + + if (isFunction(predicate)) { + get = predicate; + } else if (isString(predicate)) { if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-'; + descending = predicate.charAt(0) == '-' ? -1 : 1; predicate = predicate.substring(1); } - if (predicate === '') { - // Effectively no predicate was passed so we compare identity - return reverseComparator(compare, descending); - } - get = $parse(predicate); - if (get.constant) { - var key = get(); - return reverseComparator(function(a, b) { - return compare(a[key], b[key]); - }, descending); + if (predicate !== '') { + get = $parse(predicate); + if (get.constant) { + var key = get(); + get = function(value) { return value[key]; }; + } } } - return reverseComparator(function(a, b) { - return compare(get(a),get(b)); - }, descending); + return { get: get, descending: descending * reverseOrder }; }); - return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); + } - function comparator(o1, o2) { - for (var i = 0; i < sortPredicate.length; i++) { - var comp = sortPredicate[i](o1, o2); - if (comp !== 0) return comp; - } - return 0; - } - function reverseComparator(comp, descending) { - return descending - ? function(a, b) {return comp(b,a);} - : comp; + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; } + } - function isPrimitive(value) { - switch (typeof value) { - case 'number': /* falls through */ - case 'boolean': /* falls through */ - case 'string': - return true; - default: - return false; - } + function objectValue(value, index) { + // If `valueOf` is a valid function use that + if (typeof value.valueOf === 'function') { + value = value.valueOf(); + if (isPrimitive(value)) return value; } - - function objectToString(value) { - if (value === null) return 'null'; - if (typeof value.valueOf === 'function') { - value = value.valueOf(); - if (isPrimitive(value)) return value; - } - if (typeof value.toString === 'function') { - value = value.toString(); - if (isPrimitive(value)) return value; - } - return ''; + // If `toString` is a valid function and not the one from `Object.prototype` use that + if (hasCustomToString(value)) { + value = value.toString(); + if (isPrimitive(value)) return value; } + // We have a basic object so we use the position of the object in the collection + return index; + } - function compare(v1, v2) { - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 === t2 && t1 === "object") { - v1 = objectToString(v1); - v2 = objectToString(v2); - } - if (t1 === t2) { - if (t1 === "string") { - v1 = v1.toLowerCase(); - v2 = v2.toLowerCase(); - } - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; - } else { - return t1 < t2 ? -1 : 1; + function getPredicateValue(value, index) { + var type = typeof value; + if (value === null) { + type = 'string'; + value = 'null'; + } else if (type === 'string') { + value = value.toLowerCase(); + } else if (type === 'object') { + value = objectValue(value, index); + } + return { value: value, type: type }; + } + + function compare(v1, v2) { + var result = 0; + if (v1.type === v2.type) { + if (v1.value !== v2.value) { + result = v1.value < v2.value ? -1 : 1; } + } else { + result = v1.type < v2.type ? -1 : 1; } - }; + return result; + } } function ngDirective(directive) { @@ -17919,7 +20044,7 @@ function ngDirective(directive) { var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - if (!attr.href && !attr.xlinkHref && !attr.name) { + if (!attr.href && !attr.xlinkHref) { return function(scope, element) { // If the linked element is not an anchor tag anymore, do nothing if (element[0].nodeName.toLowerCase() !== 'a') return; @@ -18006,7 +20131,7 @@ var htmlAnchorDirective = valueFn({ }, 5000, 'page should navigate to /123'); }); - xit('should execute ng-click but not reload when href empty string and name specified', function() { + it('should execute ng-click but not reload when href empty string and name specified', function() { element(by.id('link-4')).click(); expect(element(by.model('value')).getAttribute('value')).toEqual('4'); expect(element(by.id('link-4')).getAttribute('href')).toBe(''); @@ -18051,12 +20176,12 @@ var htmlAnchorDirective = valueFn({ * * The buggy way to write it: * ```html - * <img src="http://www.gravatar.com/avatar/{{hash}}"/> + * <img src="http://www.gravatar.com/avatar/{{hash}}" alt="Description"/> * ``` * * The correct way to write it: * ```html - * <img ng-src="http://www.gravatar.com/avatar/{{hash}}"/> + * <img ng-src="http://www.gravatar.com/avatar/{{hash}}" alt="Description" /> * ``` * * @element IMG @@ -18077,12 +20202,12 @@ var htmlAnchorDirective = valueFn({ * * The buggy way to write it: * ```html - * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/> + * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description"/> * ``` * * The correct way to write it: * ```html - * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/> + * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description" /> * ``` * * @element IMG @@ -18101,25 +20226,12 @@ var htmlAnchorDirective = valueFn({ * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * * A special directive is necessary because we cannot use interpolation inside the `disabled` - * attribute. The following example would make the button enabled on Chrome/Firefox - * but not on older IEs: - * - * ```html - * <!-- See below for an example of ng-disabled being used correctly --> - * <div ng-init="isDisabled = false"> - * <button disabled="{{isDisabled}}">Disabled</button> - * </div> - * ``` - * - * This is because the HTML specification does not require browsers to preserve the values of - * boolean attributes such as `disabled` (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example <example> <file name="index.html"> - Click me to toggle: <input type="checkbox" ng-model="checked"><br/> + <label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/> <button ng-model="button" ng-disabled="checked">Button</button> </file> <file name="protractor.js" type="protractor"> @@ -18144,18 +20256,19 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy. + * + * Note that this directive should not be used together with {@link ngModel `ngModel`}, + * as this can lead to unexpected behavior. + * + * A special directive is necessary because we cannot use interpolation inside the `checked` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example <example> <file name="index.html"> - Check me to check both: <input type="checkbox" ng-model="master"><br/> - <input id="checkSlave" type="checkbox" ng-checked="master"> + <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/> + <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input"> </file> <file name="protractor.js" type="protractor"> it('should check both checkBoxes', function() { @@ -18168,7 +20281,7 @@ var htmlAnchorDirective = valueFn({ * * @element INPUT * @param {expression} ngChecked If the {@link guide/expression expression} is truthy, - * then special attribute "checked" will be set on the element + * then the `checked` attribute will be set on the element */ @@ -18179,18 +20292,17 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `readOnly` attribute on the element, if the expression inside `ngReadonly` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `readOnly` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example <example> <file name="index.html"> - Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/> - <input type="text" ng-readonly="checked" value="I'm Angular"/> + <label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/> + <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" /> </file> <file name="protractor.js" type="protractor"> it('should toggle readonly attr', function() { @@ -18214,19 +20326,17 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `selected` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example <example> <file name="index.html"> - Check me to select: <input type="checkbox" ng-model="selected"><br/> - <select> + <label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/> + <select aria-label="ngSelected demo"> <option>Hello!</option> <option id="greet" ng-selected="selected">Greetings!</option> </select> @@ -18252,17 +20362,16 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as open. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngOpen` directive solves this problem for the `open` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `open` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example <example> <file name="index.html"> - Check me check multiple: <input type="checkbox" ng-model="open"><br/> + <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/> <details id="details" ng-open="open"> <summary>Show/Hide me</summary> </details> @@ -18283,22 +20392,34 @@ var htmlAnchorDirective = valueFn({ var ngAttributeAliasDirectives = {}; - // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported if (propName == "multiple") return; + function defaultLinkFn(scope, element, attr) { + scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { + attr.$set(attrName, !!value); + }); + } + var normalized = directiveNormalize('ng-' + attrName); + var linkFn = defaultLinkFn; + + if (propName === 'checked') { + linkFn = function(scope, element, attr) { + // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input + if (attr.ngModel !== attr[normalized]) { + defaultLinkFn(scope, element, attr); + } + }; + } + ngAttributeAliasDirectives[normalized] = function() { return { restrict: 'A', priority: 100, - link: function(scope, element, attr) { - scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { - attr.$set(attrName, !!value); - }); - } + link: linkFn }; }; }); @@ -18390,6 +20511,7 @@ function nullFormRenameControl(control, name) { * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. + * @property {boolean} $pending True if at least one containing control or form is pending. * @property {boolean} $submitted True if user has submitted the form even if its invalid. * * @property {Object} $error Is an object hash, containing references to controls or @@ -18429,8 +20551,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, controls = []; - var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; - // init state form.$error = {}; form.$$success = {}; @@ -18441,8 +20561,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { form.$valid = true; form.$invalid = false; form.$submitted = false; - - parentForm.$addControl(form); + form.$$parentForm = nullFormCtrl; /** * @ngdoc method @@ -18481,11 +20600,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { /** * @ngdoc method * @name form.FormController#$addControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} * * @description - * Register a control with the form. + * Register a control with the form. Input elements using ngModelController do this automatically + * when they are linked. + * + * Note that the current state of the control will not be reflected on the new parent form. This + * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine` + * state. * - * Input elements using ngModelController do this automatically when they are linked. + * However, if the method is used programmatically, for example by adding dynamically created controls, + * or controls that have been previously removed without destroying their corresponding DOM element, + * it's the developers responsiblity to make sure the current state propagates to the parent form. + * + * For example, if an input control is added that is already `$dirty` and has `$error` properties, + * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ form.$addControl = function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored @@ -18496,6 +20627,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { if (control.$name) { form[control.$name] = control; } + + control.$$parentForm = form; }; // Private API: rename a form control @@ -18512,11 +20645,18 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { /** * @ngdoc method * @name form.FormController#$removeControl + * @param {object} control control object, either a {@link form.FormController} or an + * {@link ngModel.NgModelController} * * @description * Deregister a control from the form. * * Input elements using ngModelController do this automatically when they are destroyed. + * + * Note that only the removed control's validation state (`$errors`etc.) will be removed from the + * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be + * different from case to case. For example, removing the only `$dirty` control from a form may or + * may not mean that the form is still `$dirty`. */ form.$removeControl = function(control) { if (control.$name && form[control.$name] === control) { @@ -18533,6 +20673,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { }); arrayRemove(controls, control); + control.$$parentForm = nullFormCtrl; }; @@ -18569,7 +20710,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { delete object[property]; } }, - parentForm: parentForm, $animate: $animate }); @@ -18588,7 +20728,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { $animate.addClass(element, DIRTY_CLASS); form.$dirty = true; form.$pristine = false; - parentForm.$setDirty(); + form.$$parentForm.$setDirty(); }; /** @@ -18644,7 +20784,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { form.$setSubmitted = function() { $animate.addClass(element, SUBMITTED_CLASS); form.$submitted = true; - parentForm.$setSubmitted(); + form.$$parentForm.$setSubmitted(); }; } @@ -18683,17 +20823,14 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * In Angular, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so - * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to - * `<form>` but can be nested. This allows you to have nested forms, which is very useful when - * using Angular validation directives in forms that are dynamically generated using the - * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` - * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an - * `ngForm` directive and nest these in an outer `form` element. - * + * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to + * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group + * of controls needs to be determined. * * # CSS classes * - `ng-valid` is set if the form is valid. * - `ng-invalid` is set if the form is invalid. + * - `ng-pending` is set if the form is pending. * - `ng-pristine` is set if the form is pristine. * - `ng-dirty` is set if the form is dirty. * - `ng-submitted` is set if the form was submitted. @@ -18769,7 +20906,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { </script> <style> .my-form { - -webkit-transition:all linear 0.5s; transition:all linear 0.5s; background: transparent; } @@ -18814,10 +20950,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { - return ['$timeout', function($timeout) { + return ['$timeout', '$parse', function($timeout, $parse) { var formDirective = { name: 'form', restrict: isNgForm ? 'EAC' : 'E', + require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form controller: FormController, compile: function ngFormCompile(formElement, attr) { // Setup initial state of the control @@ -18826,7 +20963,9 @@ var formDirectiveFactory = function(isNgForm) { var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false); return { - pre: function ngFormPreLink(scope, formElement, attr, controller) { + pre: function ngFormPreLink(scope, formElement, attr, ctrls) { + var controller = ctrls[0]; + // if `action` attr is not present on the form, prevent the default action (submission) if (!('action' in attr)) { // we can't use jq events because if a form is destroyed during submission the default @@ -18855,22 +20994,24 @@ var formDirectiveFactory = function(isNgForm) { }); } - var parentFormCtrl = controller.$$parentForm; + var parentFormCtrl = ctrls[1] || controller.$$parentForm; + parentFormCtrl.$addControl(controller); + + var setter = nameAttr ? getSetter(controller.$name) : noop; if (nameAttr) { - setter(scope, null, controller.$name, controller, controller.$name); + setter(scope, controller); attr.$observe(nameAttr, function(newValue) { if (controller.$name === newValue) return; - setter(scope, null, controller.$name, undefined, controller.$name); - parentFormCtrl.$$renameControl(controller, newValue); - setter(scope, null, controller.$name, controller, controller.$name); + setter(scope, undefined); + controller.$$parentForm.$$renameControl(controller, newValue); + setter = getSetter(controller.$name); + setter(scope, controller); }); } formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (nameAttr) { - setter(scope, null, attr[nameAttr], undefined, controller.$name); - } + controller.$$parentForm.$removeControl(controller); + setter(scope, undefined); extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards }); } @@ -18879,6 +21020,14 @@ var formDirectiveFactory = function(isNgForm) { }; return formDirective; + + function getSetter(expression) { + if (expression === '') { + //create an assignable expression, so forms with an empty name can be renamed later + return $parse('this[""]').assign; + } + return $parse(expression).assign || noop; + } }]; }; @@ -18896,15 +21045,33 @@ var ngFormDirective = formDirectiveFactory(true); // Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; -var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; +// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) +// Note: We are being more lenient, because browsers are too. +// 1. Scheme +// 2. Slashes +// 3. Username +// 4. Password +// 5. Hostname +// 6. Port +// 7. Path +// 8. Query +// 9. Fragment +// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999 +var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; +var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; +var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown'; +var PARTIAL_VALIDATION_TYPES = createMap(); +forEach('date,datetime-local,month,time,week'.split(','), function(type) { + PARTIAL_VALIDATION_TYPES[type] = true; +}); + var inputType = { /** @@ -18931,9 +21098,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -18953,13 +21124,16 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - Single word: <input type="text" name="input" ng-model="example.text" - ng-pattern="example.word" required ng-trim="false"> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.pattern"> - Single word only!</span> - + <label>Single word: + <input type="text" name="input" ng-model="example.text" + ng-pattern="example.word" required ng-trim="false"> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.pattern"> + Single word only!</span> + </div> <tt>text = {{example.text}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19016,9 +21190,17 @@ var inputType = { * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO date string (yyyy-MM-dd). + * valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5 + * constraint validation. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO date string (yyyy-MM-dd). + * a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute + * (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5 + * constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -19038,13 +21220,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - Pick a date in 2013: + <label for="exampleInput">Pick a date in 2013:</label> <input type="date" id="exampleInput" name="input" ng-model="example.value" placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required /> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.date"> - Not a valid date!</span> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.date"> + Not a valid date!</span> + </div> <tt>value = {{example.value | date: "yyyy-MM-dd"}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19108,10 +21292,18 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `min` will also add native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation + * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`). + * Note that `max` will also add native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -19131,13 +21323,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - Pick a date between in 2013: + <label for="exampleInput">Pick a date between in 2013:</label> <input type="datetime-local" id="exampleInput" name="input" ng-model="example.value" placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required /> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.datetimelocal"> - Not a valid date!</span> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.datetimelocal"> + Not a valid date!</span> + </div> <tt>value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19202,10 +21396,18 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO time format (HH:mm:ss). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a - * valid ISO time format (HH:mm:ss). + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this + * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the + * `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the + * `ngMax` expression evaluates to. Note that it does not set the `max` attribute. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -19225,13 +21427,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - Pick a between 8am and 5pm: + <label for="exampleInput">Pick a between 8am and 5pm:</label> <input type="time" id="exampleInput" name="input" ng-model="example.value" placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required /> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.time"> - Not a valid date!</span> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.time"> + Not a valid date!</span> + </div> <tt>value = {{example.value | date: "HH:mm:ss"}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19295,10 +21499,18 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a - * valid ISO week format (yyyy-W##). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be - * a valid ISO week format (yyyy-W##). + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this + * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -19318,13 +21530,17 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - Pick a date between in 2013: - <input id="exampleInput" type="week" name="input" ng-model="example.value" - placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required /> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.week"> - Not a valid date!</span> + <label>Pick a date between in 2013: + <input id="exampleInput" type="week" name="input" ng-model="example.value" + placeholder="YYYY-W##" min="2012-W32" + max="2013-W52" required /> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.week"> + Not a valid date!</span> + </div> <tt>value = {{example.value | date: "yyyy-Www"}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19388,10 +21604,19 @@ var inputType = { * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. - * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be - * a valid ISO month format (yyyy-MM). - * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must - * be a valid ISO month format (yyyy-MM). + * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add + * native HTML5 constraint validation. + * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this + * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add + * native HTML5 constraint validation. + * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string + * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute. + * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string + * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute. + * @param {string=} required Sets `required` validation error key if the value is not entered. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of @@ -19411,13 +21636,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - Pick a month in 2013: + <label for="exampleInput">Pick a month in 2013:</label> <input id="exampleInput" type="month" name="input" ng-model="example.value" placeholder="yyyy-MM" min="2013-01" max="2013-12" required /> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.month"> - Not a valid month!</span> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.month"> + Not a valid month!</span> + </div> <tt>value = {{example.value | date: "yyyy-MM"}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19478,6 +21705,16 @@ var inputType = { * error docs for more information and an example of how to convert your model if necessary. * </div> * + * ## Issues with HTML5 constraint validation + * + * In browsers that follow the + * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29), + * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}. + * If a non-number is entered in the input, the browser will report the value as an empty string, + * which means the view / model values in `ngModel` and subsequently the scope value + * will also be an empty string. + * + * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. @@ -19496,9 +21733,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19514,12 +21755,16 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - Number: <input type="number" name="input" ng-model="example.value" - min="0" max="99" required> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.number"> - Not valid number!</span> + <label>Number: + <input type="number" name="input" ng-model="example.value" + min="0" max="99" required> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.number"> + Not valid number!</span> + </div> <tt>value = {{example.value}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19586,9 +21831,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19604,11 +21853,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - URL: <input type="url" name="input" ng-model="url.text" required> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.url"> - Not valid url!</span> + <label>URL: + <input type="url" name="input" ng-model="url.text" required> + <label> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.url"> + Not valid url!</span> + </div> <tt>text = {{url.text}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19677,9 +21930,13 @@ var inputType = { * as in the ngPattern directive. * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match * a RegExp found by evaluating the Angular expression given in the attribute value. - * If the expression evaluates to a RegExp object then this is used directly. - * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$` - * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @@ -19695,11 +21952,15 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - Email: <input type="email" name="input" ng-model="email.text" required> - <span class="error" ng-show="myForm.input.$error.required"> - Required!</span> - <span class="error" ng-show="myForm.input.$error.email"> - Not valid email!</span> + <label>Email: + <input type="email" name="input" ng-model="email.text" required> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.input.$error.required"> + Required!</span> + <span class="error" ng-show="myForm.input.$error.email"> + Not valid email!</span> + </div> <tt>text = {{email.text}}</tt><br/> <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> @@ -19745,12 +22006,15 @@ var inputType = { * HTML radio button. * * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string} value The value to which the expression should be set when selected. + * @param {string} value The value to which the `ngModel` expression should be set when selected. + * Note that `value` only supports `string` values, i.e. the scope model needs to be a string, + * too. Use `ngValue` if you need complex models (`number`, `object`, ...). * @param {string=} name Property name of the form under which the control is published. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. - * @param {string} ngValue Angular expression which sets the value to which the expression should - * be set when selected. + * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio + * is selected. Should be used instead of the `value` attribute if you need + * a non-string `ngModel` (`boolean`, `array`, ...). * * @example <example name="radio-input-directive" module="radioExample"> @@ -19768,9 +22032,18 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - <input type="radio" ng-model="color.name" value="red"> Red <br/> - <input type="radio" ng-model="color.name" ng-value="specialValue"> Green <br/> - <input type="radio" ng-model="color.name" value="blue"> Blue <br/> + <label> + <input type="radio" ng-model="color.name" value="red"> + Red + </label><br/> + <label> + <input type="radio" ng-model="color.name" ng-value="specialValue"> + Green + </label><br/> + <label> + <input type="radio" ng-model="color.name" value="blue"> + Blue + </label><br/> <tt>color = {{color.name | json}}</tt><br/> </form> Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`. @@ -19818,9 +22091,13 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="ExampleController"> - Value1: <input type="checkbox" ng-model="checkboxModel.value1"> <br/> - Value2: <input type="checkbox" ng-model="checkboxModel.value2" - ng-true-value="'YES'" ng-false-value="'NO'"> <br/> + <label>Value1: + <input type="checkbox" ng-model="checkboxModel.value1"> + </label><br/> + <label>Value2: + <input type="checkbox" ng-model="checkboxModel.value2" + ng-true-value="'YES'" ng-false-value="'NO'"> + </label><br/> <tt>value1 = {{checkboxModel.value1}}</tt><br/> <tt>value2 = {{checkboxModel.value2}}</tt><br/> </form> @@ -19881,6 +22158,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -19910,8 +22189,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { - var timeout; - var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { @@ -19943,8 +22220,32 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); + } + }); + } + }); + } + ctrl.$render = function() { - element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); + // Workaround for Firefox validation #12102. + var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; + if (element.val() !== value) { + element.val(value); + } }; } @@ -20045,8 +22346,8 @@ function createDateInputType(type, regexp, parseDate, format) { // parser/formatter in the processing chain so that the model // contains some different data format! var parsedDate = parseDate(value, previousDate); - if (timezone === 'UTC') { - parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset()); + if (timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); } return parsedDate; } @@ -20059,9 +22360,8 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone === 'UTC') { - var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); - previousDate = new Date(previousDate.getTime() + timezoneOffset); + if (previousDate && timezone) { + previousDate = convertTimezoneToLocal(previousDate, timezone, true); } return $filter('date')(value, format, timezone); } else { @@ -20098,7 +22398,7 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseObservedDateValue(val) { - return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; + return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; } }; } @@ -20285,9 +22585,15 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -20318,9 +22624,15 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the - * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for - * patterns defined as scope expressions. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match + * a RegExp found by evaluating the Angular expression given in the attribute value. + * If the expression evaluates to a RegExp object, then this is used directly. + * If the expression evaluates to a string, then it will be converted to a RegExp + * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to + * `new RegExp('^abc$')`.<br /> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input. @@ -20338,26 +22650,36 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt </script> <div ng-controller="ExampleController"> <form name="myForm"> - User name: <input type="text" name="userName" ng-model="user.name" required> - <span class="error" ng-show="myForm.userName.$error.required"> - Required!</span><br> - Last name: <input type="text" name="lastName" ng-model="user.last" - ng-minlength="3" ng-maxlength="10"> - <span class="error" ng-show="myForm.lastName.$error.minlength"> - Too short!</span> - <span class="error" ng-show="myForm.lastName.$error.maxlength"> - Too long!</span><br> + <label> + User name: + <input type="text" name="userName" ng-model="user.name" required> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.userName.$error.required"> + Required!</span> + </div> + <label> + Last name: + <input type="text" name="lastName" ng-model="user.last" + ng-minlength="3" ng-maxlength="10"> + </label> + <div role="alert"> + <span class="error" ng-show="myForm.lastName.$error.minlength"> + Too short!</span> + <span class="error" ng-show="myForm.lastName.$error.maxlength"> + Too long!</span> + </div> </form> <hr> <tt>user = {{user}}</tt><br/> - <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br> - <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br> - <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br> - <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br> - <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br> - <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br> + <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br/> + <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br/> + <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br/> + <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br/> + <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br/> </div> </file> <file name="protractor.js" type="protractor"> @@ -20546,7 +22868,7 @@ var ngValueDirective = function() { }]); </script> <div ng-controller="ExampleController"> - Enter name: <input type="text" ng-model="name"><br> + <label>Enter name: <input type="text" ng-model="name"></label><br> Hello <span ng-bind="name"></span>! </div> </file> @@ -20571,7 +22893,7 @@ var ngBindDirective = ['$compile', function($compile) { $compile.$$addBindingInfo(element, attr.ngBind); element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - element.textContent = value === undefined ? '' : value; + element.textContent = isUndefined(value) ? '' : value; }); }; } @@ -20607,8 +22929,8 @@ var ngBindDirective = ['$compile', function($compile) { }]); </script> <div ng-controller="ExampleController"> - Salutation: <input type="text" ng-model="salutation"><br> - Name: <input type="text" ng-model="name"><br> + <label>Salutation: <input type="text" ng-model="salutation"></label><br> + <label>Name: <input type="text" ng-model="name"></label><br> <pre ng-bind-template="{{salutation}} {{name}}!"></pre> </div> </file> @@ -20639,7 +22961,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate $compile.$$addBindingInfo(element, interpolateFn.expressions); element = element[0]; attr.$observe('ngBindTemplate', function(value) { - element.textContent = value === undefined ? '' : value; + element.textContent = isUndefined(value) ? '' : value; }); }; } @@ -20833,7 +23155,9 @@ function classDirective(name, selector) { } function digestClassCounts(classes, count) { - var classCounts = element.data('$classCounts') || {}; + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + var classCounts = element.data('$classCounts') || createMap(); var classesToUpdate = []; forEach(classes, function(className) { if (count > 0 || classCounts[className]) { @@ -20890,12 +23214,15 @@ function classDirective(name, selector) { } function arrayClasses(classVal) { + var classes = []; if (isArray(classVal)) { - return classVal; + forEach(classVal, function(v) { + classes = classes.concat(arrayClasses(v)); + }); + return classes; } else if (isString(classVal)) { return classVal.split(' '); } else if (isObject(classVal)) { - var classes = []; forEach(classVal, function(v, k) { if (v) { classes = classes.concat(k.split(' ')); @@ -20923,16 +23250,18 @@ function classDirective(name, selector) { * 1. If the expression evaluates to a string, the string should be one or more space-delimited class * names. * - * 2. If the expression evaluates to an array, each element of the array should be a string that is - * one or more space-delimited class names. - * - * 3. If the expression evaluates to an object, then for each key-value pair of the + * 2. If the expression evaluates to an object, then for each key-value pair of the * object with a truthy value the corresponding key is used as a class name. * + * 3. If the expression evaluates to an array, each element of the array should either be a string as in + * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array + * to give you more control over what CSS classes appear. See the code below for an example of this. + * + * * The directive won't add duplicate classes if a particular class was already set. * - * When the expression changes, the previously added classes are removed and only then the - * new classes are added. + * When the expression changes, the previously added classes are removed and only then are the + * new classes added. * * @animations * **add** - happens just before the class is applied to the elements @@ -20949,22 +23278,39 @@ function classDirective(name, selector) { * @example Example that demonstrates basic bindings via ngClass directive. <example> <file name="index.html"> - <p ng-class="{strike: deleted, bold: important, red: error}">Map Syntax Example</p> - <input type="checkbox" ng-model="deleted"> deleted (apply "strike" class)<br> - <input type="checkbox" ng-model="important"> important (apply "bold" class)<br> - <input type="checkbox" ng-model="error"> error (apply "red" class) + <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p> + <label> + <input type="checkbox" ng-model="deleted"> + deleted (apply "strike" class) + </label><br> + <label> + <input type="checkbox" ng-model="important"> + important (apply "bold" class) + </label><br> + <label> + <input type="checkbox" ng-model="error"> + error (apply "has-error" class) + </label> <hr> <p ng-class="style">Using String Syntax</p> - <input type="text" ng-model="style" placeholder="Type: bold strike red"> + <input type="text" ng-model="style" + placeholder="Type: bold strike red" aria-label="Type: bold strike red"> <hr> <p ng-class="[style1, style2, style3]">Using Array Syntax</p> - <input ng-model="style1" placeholder="Type: bold, strike or red"><br> - <input ng-model="style2" placeholder="Type: bold, strike or red"><br> - <input ng-model="style3" placeholder="Type: bold, strike or red"><br> + <input ng-model="style1" + placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red"><br> + <input ng-model="style2" + placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 2"><br> + <input ng-model="style3" + placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 3"><br> + <hr> + <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p> + <input ng-model="style4" placeholder="Type: bold, strike" aria-label="Type: bold, strike"><br> + <label><input type="checkbox" ng-model="warning"> warning (apply "orange" class)</label> </file> <file name="style.css"> .strike { - text-decoration: line-through; + text-decoration: line-through; } .bold { font-weight: bold; @@ -20972,6 +23318,13 @@ function classDirective(name, selector) { .red { color: red; } + .has-error { + color: red; + background-color: yellow; + } + .orange { + color: orange; + } </file> <file name="protractor.js" type="protractor"> var ps = element.all(by.css('p')); @@ -20979,13 +23332,13 @@ function classDirective(name, selector) { it('should let you toggle the class', function() { expect(ps.first().getAttribute('class')).not.toMatch(/bold/); - expect(ps.first().getAttribute('class')).not.toMatch(/red/); + expect(ps.first().getAttribute('class')).not.toMatch(/has-error/); element(by.model('important')).click(); expect(ps.first().getAttribute('class')).toMatch(/bold/); element(by.model('error')).click(); - expect(ps.first().getAttribute('class')).toMatch(/red/); + expect(ps.first().getAttribute('class')).toMatch(/has-error/); }); it('should let you toggle string example', function() { @@ -20996,11 +23349,18 @@ function classDirective(name, selector) { }); it('array example should have 3 classes', function() { - expect(ps.last().getAttribute('class')).toBe(''); + expect(ps.get(2).getAttribute('class')).toBe(''); element(by.model('style1')).sendKeys('bold'); element(by.model('style2')).sendKeys('strike'); element(by.model('style3')).sendKeys('red'); - expect(ps.last().getAttribute('class')).toBe('bold strike red'); + expect(ps.get(2).getAttribute('class')).toBe('bold strike red'); + }); + + it('array with map example should have 2 classes', function() { + expect(ps.last().getAttribute('class')).toBe(''); + element(by.model('style4')).sendKeys('bold'); + element(by.model('warning')).click(); + expect(ps.last().getAttribute('class')).toBe('bold orange'); }); </file> </example> @@ -21018,7 +23378,6 @@ function classDirective(name, selector) { </file> <file name="style.css"> .base-class { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -21050,8 +23409,8 @@ function classDirective(name, selector) { The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure - to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and - {@link ng.$animate#removeClass $animate.removeClass}. + to view the step by step details of {@link $animate#addClass $animate.addClass} and + {@link $animate#removeClass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); @@ -21274,20 +23633,20 @@ var ngCloakDirective = ngDirective({ * <example name="ngControllerAs" module="controllerAsExample"> * <file name="index.html"> * <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings"> - * Name: <input type="text" ng-model="settings.name"/> - * [ <a href="" ng-click="settings.greet()">greet</a> ]<br/> + * <label>Name: <input type="text" ng-model="settings.name"/></label> + * <button ng-click="settings.greet()">greet</button><br/> * Contact: * <ul> * <li ng-repeat="contact in settings.contacts"> - * <select ng-model="contact.type"> + * <select ng-model="contact.type" aria-label="Contact method" id="select_{{$index}}"> * <option>phone</option> * <option>email</option> * </select> - * <input type="text" ng-model="contact.value"/> - * [ <a href="" ng-click="settings.clearContact(contact)">clear</a> - * | <a href="" ng-click="settings.removeContact(contact)">X</a> ] + * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" /> + * <button ng-click="settings.clearContact(contact)">clear</button> + * <button ng-click="settings.removeContact(contact)" aria-label="Remove">X</button> * </li> - * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> + * <li><button ng-click="settings.addContact()">add</button></li> * </ul> * </div> * </file> @@ -21337,12 +23696,12 @@ var ngCloakDirective = ngDirective({ * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.element(by.linkText('clear')).click(); + * firstRepeat.element(by.buttonText('clear')).click(); * * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.element(by.linkText('add')).click(); + * container.element(by.buttonText('add')).click(); * * expect(container.element(by.repeater('contact in settings.contacts').row(2)) * .element(by.model('contact.value')) @@ -21357,20 +23716,20 @@ var ngCloakDirective = ngDirective({ * <example name="ngController" module="controllerExample"> * <file name="index.html"> * <div id="ctrl-exmpl" ng-controller="SettingsController2"> - * Name: <input type="text" ng-model="name"/> - * [ <a href="" ng-click="greet()">greet</a> ]<br/> + * <label>Name: <input type="text" ng-model="name"/></label> + * <button ng-click="greet()">greet</button><br/> * Contact: * <ul> * <li ng-repeat="contact in contacts"> - * <select ng-model="contact.type"> + * <select ng-model="contact.type" id="select_{{$index}}"> * <option>phone</option> * <option>email</option> * </select> - * <input type="text" ng-model="contact.value"/> - * [ <a href="" ng-click="clearContact(contact)">clear</a> - * | <a href="" ng-click="removeContact(contact)">X</a> ] + * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" /> + * <button ng-click="clearContact(contact)">clear</button> + * <button ng-click="removeContact(contact)">X</button> * </li> - * <li>[ <a href="" ng-click="addContact()">add</a> ]</li> + * <li>[ <button ng-click="addContact()">add</button> ]</li> * </ul> * </div> * </file> @@ -21420,12 +23779,12 @@ var ngCloakDirective = ngDirective({ * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe('john.smith@example.org'); * - * firstRepeat.element(by.linkText('clear')).click(); + * firstRepeat.element(by.buttonText('clear')).click(); * * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) * .toBe(''); * - * container.element(by.linkText('add')).click(); + * container.element(by.buttonText('add')).click(); * * expect(container.element(by.repeater('contact in contacts').row(2)) * .element(by.model('contact.value')) @@ -21451,27 +23810,29 @@ var ngControllerDirective = [function() { * * @element html * @description - * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. + * + * Angular has some features that can break certain + * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules. + * + * If you intend to implement these rules then you must tell Angular not to use these features. * * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps. * - * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things). - * For Angular to be CSP compatible there are only two things that we need to do differently: * - * - don't use `Function` constructor to generate optimized value getters - * - don't inject custom stylesheet into the document + * The following rules affect Angular: * - * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp` - * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will - * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will - * be raised. + * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions + * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30% + * increase in the speed of evaluating Angular expressions. * - * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically - * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}). - * To make those directives work in CSP mode, include the `angular-csp.css` manually. + * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular + * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}). + * To make these directives work when a CSP rule is blocking inline styles, you must link to the + * `angular-csp.css` in your HTML manually. * - * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This - * autodetection however triggers a CSP error to be logged in the console: + * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval + * and automatically deactivates this feature in the {@link $parse} service. This autodetection, + * however, triggers a CSP error to be logged in the console: * * ``` * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of @@ -21480,11 +23841,39 @@ var ngControllerDirective = [function() { * ``` * * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp` - * directive on the root element of the application or on the `angular.js` script tag, whichever - * appears first in the html document. + * directive on an element of the HTML document that appears before the `<script>` tag that loads + * the `angular.js` file. * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * + * You can specify which of the CSP related Angular features should be deactivated by providing + * a value for the `ng-csp` attribute. The options are as follows: + * + * * no-inline-style: this stops Angular from injecting CSS styles into the DOM + * + * * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings + * + * You can use these values in the following combinations: + * + * + * * No declaration means that Angular will assume that you can do inline styles, but it will do + * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions + * of Angular. + * + * * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline + * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions + * of Angular. + * + * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject + * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`. + * + * * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can + * run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">` + * + * * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject + * styles nor use eval, which is the same as an empty: ng-csp. + * E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">` + * * @example * This example shows how to apply the `ngCsp` directive to the `html` tag. ```html @@ -21616,7 +24005,7 @@ var ngControllerDirective = [function() { // ngCsp is not implemented as a proper directive any more, because we need it be processed while we // bootstrap the system (before $parse is instantiated), for this reason we just have -// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc +// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc /** * @ngdoc directive @@ -22106,6 +24495,7 @@ forEach( * @ngdoc directive * @name ngIf * @restrict A + * @multiElement * * @description * The `ngIf` directive removes or recreates a portion of the DOM tree based on an @@ -22148,7 +24538,7 @@ forEach( * @example <example module="ngAnimate" deps="angular-animate.js" animations="true"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> + <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/> Show when checked: <span ng-if="checked" class="animate-if"> This is removed when the checkbox is unchecked. @@ -22162,7 +24552,6 @@ forEach( } .animate-if.ng-enter, .animate-if.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } @@ -22260,7 +24649,13 @@ var ngIfDirective = ['$animate', function($animate) { * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant, * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`. * @param {string=} onload Expression to evaluate when a new partial is loaded. - * + * <div class="alert alert-warning"> + * **Note:** When using onload on SVG elements in IE11, the browser will try to call + * a function with the name on the window element, which will usually throw a + * "function is undefined" error. To fix this, you can instead use `data-onload` or a + * different form that {@link guide/directive#normalization matches} `onload`. + * </div> + * * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll * $anchorScroll} to scroll the viewport after the content is loaded. * @@ -22311,7 +24706,6 @@ var ngIfDirective = ['$animate', function($animate) { } .slide-animate.ng-enter, .slide-animate.ng-leave { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; position:absolute; @@ -22453,6 +24847,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', //set the 2nd param to true to ignore the template request error so that the inner //contents and scope can be cleaned up. $templateRequest(src, true).then(function(response) { + if (scope.$$destroyed) return; + if (thisChangeId !== changeCounter) return; var newScope = scope.$new(); ctrl.template = response; @@ -22474,6 +24870,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', currentScope.$emit('$includeContentLoaded', src); scope.$eval(onloadExp); }, function() { + if (scope.$$destroyed) return; + if (thisChangeId === changeCounter) { cleanupLastIncludeContent(); scope.$emit('$includeContentError', src); @@ -22529,17 +24927,19 @@ var ngIncludeFillContentDirective = ['$compile', * The `ngInit` directive allows you to evaluate an expression in the * current scope. * - * <div class="alert alert-error"> - * The only appropriate use of `ngInit` is for aliasing special properties of - * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you - * should use {@link guide/controller controllers} rather than `ngInit` - * to initialize values on a scope. + * <div class="alert alert-danger"> + * This directive can be abused to add unnecessary amounts of logic into your templates. + * There are only a few appropriate uses of `ngInit`, such as for aliasing special properties of + * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below; and for injecting data via + * server side scripting. Besides these few cases, you should use {@link guide/controller controllers} + * rather than `ngInit` to initialize values on a scope. * </div> + * * <div class="alert alert-warning"> - * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make - * sure you have parenthesis for correct precedence: + * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make + * sure you have parentheses to ensure correct operator precedence: * <pre class="prettyprint"> - * `<div ng-init="test1 = (data | orderBy:'name')"></div>` + * `<div ng-init="test1 = ($index | toString)"></div>` * </pre> * </div> * @@ -22616,9 +25016,11 @@ var ngInitDirective = ngDirective({ * </file> * <file name="index.html"> * <form name="myForm" ng-controller="ExampleController"> - * List: <input name="namesInput" ng-model="names" ng-list required> - * <span class="error" ng-show="myForm.namesInput.$error.required"> + * <label>List: <input name="namesInput" ng-model="names" ng-list required></label> + * <span role="alert"> + * <span class="error" ng-show="myForm.namesInput.$error.required"> * Required!</span> + * </span> * <br> * <tt>names = {{names}}</tt><br/> * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> @@ -22650,7 +25052,7 @@ var ngInitDirective = ngDirective({ * </file> * </example> * - * ### Example - splitting on whitespace + * ### Example - splitting on newline * <example name="ngList-directive-newlines"> * <file name="index.html"> * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> @@ -22735,7 +25137,9 @@ var ngModelMinErr = minErr('ngModel'); * @ngdoc type * @name ngModel.NgModelController * - * @property {string} $viewValue Actual string value in the view. + * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a + * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue + * is set. * @property {*} $modelValue The value in the model that the control is bound to. * @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. The functions are called in array order, each passing @@ -22904,7 +25308,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. required>Change me!</div> <span ng-show="myForm.myWidget.$error.required">Required!</span> <hr> - <textarea ng-model="userContent"></textarea> + <textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea> </form> </file> <file name="protractor.js" type="protractor"> @@ -22949,7 +25353,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$$success = {}; // keep valid keys here this.$pending = undefined; // keep pending keys here this.$name = $interpolate($attr.name || '', false)($scope); - + this.$$parentForm = nullFormCtrl; var parsedNgModel = $parse($attr.ngModel), parsedNgModelAssign = parsedNgModel.assign, @@ -22998,10 +25402,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last * committed value then `$render()` is called to update the input control. * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and - * the `$viewValue` are different to last time. + * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different to their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ @@ -23018,7 +25422,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`. * - * You can override this for input directives whose concept of being empty is different to the + * You can override this for input directives whose concept of being empty is different from the * default. The `checkboxInputType` directive does this because in its case a value of `false` * implies empty. * @@ -23029,8 +25433,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ return isUndefined(value) || value === '' || value === null || value !== value; }; - var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - currentValidationRunId = 0; + var currentValidationRunId = 0; /** * @ngdoc method @@ -23063,7 +25466,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ unset: function(object, property) { delete object[property]; }, - parentForm: parentForm, $animate: $animate }); @@ -23101,7 +25503,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$pristine = false; $animate.removeClass($element, PRISTINE_CLASS); $animate.addClass($element, DIRTY_CLASS); - parentForm.$setDirty(); + ctrl.$$parentForm.$setDirty(); }; /** @@ -23148,11 +25550,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * which may be caused by a pending debounced event or because the input is waiting for a some * future event. * - * If you have an input that uses `ng-model-options` to set up debounced events or events such - * as blur you can have a situation where there is a period when the `$viewValue` - * is out of synch with the ngModel's `$modelValue`. + * If you have an input that uses `ng-model-options` to set up debounced updates or updates that + * depend on special events such as blur, you can have a situation where there is a period when + * the `$viewValue` is out of sync with the ngModel's `$modelValue`. + * + * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update + * and reset the input to the last committed view value. * - * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` + * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` * programmatically before these debounced/future events have resolved/occurred, because Angular's * dirty checking mechanism is not able to tell whether the model has actually changed or not. * @@ -23165,37 +25570,63 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * angular.module('cancel-update-example', []) * * .controller('CancelUpdateController', ['$scope', function($scope) { - * $scope.resetWithCancel = function(e) { - * if (e.keyCode == 27) { - * $scope.myForm.myInput1.$rollbackViewValue(); - * $scope.myValue = ''; - * } - * }; - * $scope.resetWithoutCancel = function(e) { + * $scope.model = {}; + * + * $scope.setEmpty = function(e, value, rollback) { * if (e.keyCode == 27) { - * $scope.myValue = ''; + * e.preventDefault(); + * if (rollback) { + * $scope.myForm[value].$rollbackViewValue(); + * } + * $scope.model[value] = ''; * } * }; * }]); * </file> * <file name="index.html"> * <div ng-controller="CancelUpdateController"> - * <p>Try typing something in each input. See that the model only updates when you - * blur off the input. - * </p> - * <p>Now see what happens if you start typing then press the Escape key</p> + * <p>Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:</p> + * <ol> + * <li>Type something in the input. You will see that the model is not yet updated</li> + * <li>Press the Escape key. + * <ol> + * <li> In the first example, nothing happens, because the model is already '', and no + * update is detected. If you blur the input, the model will be set to the current view. + * </li> + * <li> In the second example, the pending update is cancelled, and the input is set back + * to the last committed view value (''). Blurring the input does nothing. + * </li> + * </ol> + * </li> + * </ol> * * <form name="myForm" ng-model-options="{ updateOn: 'blur' }"> - * <p>With $rollbackViewValue()</p> - * <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/> - * myValue: "{{ myValue }}" - * - * <p>Without $rollbackViewValue()</p> - * <input name="myInput2" ng-model="myValue" ng-keydown="resetWithoutCancel($event)"><br/> - * myValue: "{{ myValue }}" + * <div> + * <p id="inputDescription1">Without $rollbackViewValue():</p> + * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1" + * ng-keydown="setEmpty($event, 'value1')"> + * value1: "{{ model.value1 }}" + * </div> + * + * <div> + * <p id="inputDescription2">With $rollbackViewValue():</p> + * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2" + * ng-keydown="setEmpty($event, 'value2', true)"> + * value2: "{{ model.value2 }}" + * </div> * </form> * </div> * </file> + <file name="style.css"> + div { + display: table-cell; + } + div:nth-child(1) { + padding-right: 30px; + } + + </file> * </example> */ this.$rollbackViewValue = function() { @@ -23269,7 +25700,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processParseErrors() { var errorKey = ctrl.$$parserName || 'parse'; - if (parserValid === undefined) { + if (isUndefined(parserValid)) { setValidity(errorKey, null); } else { if (!parserValid) { @@ -23309,7 +25740,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ forEach(ctrl.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { - throw ngModelMinErr("$asyncValidators", + throw ngModelMinErr('nopromise', "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); } setValidity(name, undefined); @@ -23439,37 +25870,47 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Update the view value. * - * This method should be called when an input directive want to change the view value; typically, - * this is done from within a DOM event handler. - * - * For example {@link ng.directive:input input} calls it when the value of the input changes and - * {@link ng.directive:select select} calls it when an option is selected. + * This method should be called when a control wants to change the view value; typically, + * this is done from within a DOM event handler. For example, the {@link ng.directive:input input} + * directive calls it when the value of the input changes and {@link ng.directive:select select} + * calls it when an option is selected. * - * If the new `value` is an object (rather than a string or a number), we should make a copy of the - * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep - * watch of objects, it only looks for a change of identity. If you only change the property of - * the object then ngModel will not realise that the object has changed and will not invoke the - * `$parsers` and `$validators` pipelines. - * - * For this reason, you should not change properties of the copy once it has been passed to - * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly. - * - * When this method is called, the new `value` will be staged for committing through the `$parsers` + * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged * value sent directly for processing, finally to be applied to `$modelValue` and then the - * **expression** specified in the `ng-model` attribute. - * - * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called. + * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners, + * in the `$viewChangeListeners` list, are called. * * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn` * and the `default` trigger is not listed, all those actions will remain pending until one of the * `updateOn` events is triggered on the DOM element. * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions} * directive is used with a custom debounce for this particular event. + * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce` + * is specified, once the timer runs out. + * + * When used with standard inputs, the view value will always be a string (which is in some cases + * parsed into another type, such as a `Date` object for `input[date]`.) + * However, custom controls might also pass objects to this method. In this case, we should make + * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not + * perform a deep watch of objects, it only looks for a change of identity. If you only change + * the property of the object then ngModel will not realise that the object has changed and + * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should + * not change properties of the copy once it has been passed to `$setViewValue`. + * Otherwise you may cause the model value on the scope to change incorrectly. + * + * <div class="alert alert-info"> + * In any case, the value passed to the method should always reflect the current value + * of the control. For example, if you are calling `$setViewValue` for an input element, + * you should pass the input DOM value. Otherwise, the control and the scope model become + * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change + * the control's DOM value in any way. If we want to change the control's DOM value + * programmatically, we should update the `ngModel` scope expression. Its new value will be + * picked up by the model controller, which will run it through the `$formatters`, `$render` it + * to update the DOM, and finally call `$validate` on it. + * </div> * - * Note that calling this function does not trigger a `$digest`. - * - * @param {string} value Value from the view. + * @param {*} value value from the view. * @param {string} trigger Event that triggered the update. */ this.$setViewValue = function(value, trigger) { @@ -23595,6 +26036,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * + * # Complex Models (objects or collections) + * + * By default, `ngModel` watches the model by reference, not value. This is important to know when + * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the + * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered. + * + * The model must be assigned an entirely new object or collection before a re-rendering will occur. + * + * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression + * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or + * if the select is given the `multiple` attribute. + * + * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the + * first level of the object (or only changing the properties of an item in the collection if it's an array) will still + * not trigger a re-rendering of the model. + * * # CSS classes * The following CSS classes are added and removed on the associated input/select/textarea element * depending on the validity of the model. @@ -23646,7 +26103,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </script> <style> .my-input { - -webkit-transition:all linear 0.5s; transition:all linear 0.5s; background: transparent; } @@ -23655,10 +26111,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ background: red; } </style> - Update input to see transitions when valid/invalid. - Integer is a valid value. + <p id="inputDescription"> + Update input to see transitions when valid/invalid. + Integer is a valid value. + </p> <form name="testForm" ng-controller="ExampleController"> - <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" /> + <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" + aria-describedby="inputDescription" /> </form> </file> * </example> @@ -23668,7 +26127,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a * function that returns a representation of the model when called with zero arguments, and sets * the internal state of a model when called with an argument. It's sometimes useful to use this - * for models that have an internal representation that's different than what the model exposes + * for models that have an internal representation that's different from what the model exposes * to the view. * * <div class="alert alert-success"> @@ -23688,10 +26147,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ <file name="index.html"> <div ng-controller="ExampleController"> <form name="userForm"> - Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ getterSetter: true }" /> + <label>Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </label> </form> <pre>user.name = <span ng-bind="user.name()"></span></pre> </div> @@ -23729,7 +26189,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) { return { pre: function ngModelPreLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; + formCtrl = ctrls[1] || modelCtrl.$$parentForm; modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); @@ -23738,12 +26198,12 @@ var ngModelDirective = ['$rootScope', function($rootScope) { attr.$observe('name', function(newValue) { if (modelCtrl.$name !== newValue) { - formCtrl.$$renameControl(modelCtrl, newValue); + modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue); } }); scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); + modelCtrl.$$parentForm.$removeControl(modelCtrl); }); }, post: function ngModelPostLink(scope, element, attr, ctrls) { @@ -23781,7 +26241,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; * takes place when a timer expires; this timer will be reset after another change takes place. * * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might - * be different than the value in the actual model. This means that if you update the model you + * be different from the value in the actual model. This means that if you update the model you * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in * order to make sure it is synchronized with the model and that any debounced action is canceled. * @@ -23803,14 +26263,16 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; * - `debounce`: integer value which contains the debounce model update value in milliseconds. A * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a * custom value for each event. For example: - * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"` + * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"` * - `allowInvalid`: boolean value which indicates that the model can be set with values that did * not validate correctly instead of the default behavior of setting the model to undefined. * - `getterSetter`: boolean value which determines whether or not to treat functions bound to `ngModel` as getters/setters. * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for - * `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`, - * otherwise the default timezone of the browser will be used. + * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the + * continental US time zone abbreviations, but for general use, use a time zone offset, for + * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) + * If not specified, the timezone of the browser will be used. * * @example @@ -23822,22 +26284,24 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; <file name="index.html"> <div ng-controller="ExampleController"> <form name="userForm"> - Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ updateOn: 'blur' }" - ng-keyup="cancel($event)" /><br /> - - Other data: - <input type="text" ng-model="user.data" /><br /> + <label>Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ updateOn: 'blur' }" + ng-keyup="cancel($event)" /> + </label><br /> + <label>Other data: + <input type="text" ng-model="user.data" /> + </label><br /> </form> <pre>user.name = <span ng-bind="user.name"></span></pre> + <pre>user.data = <span ng-bind="user.data"></span></pre> </div> </file> <file name="app.js"> angular.module('optionsExample', []) .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'say', data: '' }; + $scope.user = { name: 'John', data: '' }; $scope.cancel = function(e) { if (e.keyCode == 27) { @@ -23852,20 +26316,20 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; var other = element(by.model('user.data')); it('should allow custom events', function() { - input.sendKeys(' hello'); + input.sendKeys(' Doe'); input.click(); - expect(model.getText()).toEqual('say'); + expect(model.getText()).toEqual('John'); other.click(); - expect(model.getText()).toEqual('say hello'); + expect(model.getText()).toEqual('John Doe'); }); it('should $rollbackViewValue when model changes', function() { - input.sendKeys(' hello'); - expect(input.getAttribute('value')).toEqual('say hello'); + input.sendKeys(' Doe'); + expect(input.getAttribute('value')).toEqual('John Doe'); input.sendKeys(protractor.Key.ESCAPE); - expect(input.getAttribute('value')).toEqual('say'); + expect(input.getAttribute('value')).toEqual('John'); other.click(); - expect(model.getText()).toEqual('say'); + expect(model.getText()).toEqual('John'); }); </file> </example> @@ -23877,11 +26341,13 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; <file name="index.html"> <div ng-controller="ExampleController"> <form name="userForm"> - Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ debounce: 1000 }" /> - <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br /> + <label>Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ debounce: 1000 }" /> + </label> + <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button> + <br /> </form> <pre>user.name = <span ng-bind="user.name"></span></pre> </div> @@ -23889,7 +26355,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; <file name="app.js"> angular.module('optionsExample', []) .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'say' }; + $scope.user = { name: 'Igor' }; }]); </file> </example> @@ -23900,10 +26366,11 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; <file name="index.html"> <div ng-controller="ExampleController"> <form name="userForm"> - Name: - <input type="text" name="userName" - ng-model="user.name" - ng-model-options="{ getterSetter: true }" /> + <label>Name: + <input type="text" name="userName" + ng-model="user.name" + ng-model-options="{ getterSetter: true }" /> + </label> </form> <pre>user.name = <span ng-bind="user.name()"></span></pre> </div> @@ -23930,9 +26397,9 @@ var ngModelOptionsDirective = function() { restrict: 'A', controller: ['$scope', '$attrs', function($scope, $attrs) { var that = this; - this.$options = $scope.$eval($attrs.ngModelOptions); + this.$options = copy($scope.$eval($attrs.ngModelOptions)); // Allow adding/overriding bound events - if (this.$options.updateOn !== undefined) { + if (isDefined(this.$options.updateOn)) { this.$options.updateOnDefault = false; // extract "default" pseudo-event from list of events that can trigger a model update this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() { @@ -23955,7 +26422,6 @@ function addSetValidityMethod(context) { classCache = {}, set = context.set, unset = context.unset, - parentForm = context.parentForm, $animate = context.$animate; classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); @@ -23963,7 +26429,7 @@ function addSetValidityMethod(context) { ctrl.$setValidity = setValidity; function setValidity(validationErrorKey, state, controller) { - if (state === undefined) { + if (isUndefined(state)) { createAndSet('$pending', validationErrorKey, controller); } else { unsetAndCleanup('$pending', validationErrorKey, controller); @@ -24007,7 +26473,7 @@ function addSetValidityMethod(context) { } toggleValidationCss(validationErrorKey, combinedState); - parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl); } function createAndSet(name, value, controller) { @@ -24047,7 +26513,9 @@ function addSetValidityMethod(context) { function isObjectEmpty(obj) { if (obj) { for (var prop in obj) { - return false; + if (obj.hasOwnProperty(prop)) { + return false; + } } } return true; @@ -24087,6 +26555,775 @@ function isObjectEmpty(obj) { */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); +/* global jqLiteRemove */ + +var ngOptionsMinErr = minErr('ngOptions'); + +/** + * @ngdoc directive + * @name ngOptions + * @restrict A + * + * @description + * + * The `ngOptions` attribute can be used to dynamically generate a list of `<option>` + * elements for the `<select>` element using the array or object obtained by evaluating the + * `ngOptions` comprehension expression. + * + * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a + * similar result. However, `ngOptions` provides some benefits such as reducing memory and + * increasing speed by not creating a new scope for each repeated instance, as well as providing + * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound + * to a non-string value. This is because an option element can only be bound to string values at + * present. + * + * When an item in the `<select>` menu is selected, the array element or object property + * represented by the selected option will be bound to the model identified by the `ngModel` + * directive. + * + * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can + * be nested into the `<select>` element. This element will then represent the `null` or "not selected" + * option. See example below for demonstration. + * + * ## Complex Models (objects or collections) + * + * By default, `ngModel` watches the model by reference, not value. This is important to know when + * binding the select to a model that is an object or a collection. + * + * One issue occurs if you want to preselect an option. For example, if you set + * the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection, + * because the objects are not identical. So by default, you should always reference the item in your collection + * for preselections, e.g.: `$scope.selected = $scope.collection[3]`. + * + * Another solution is to use a `track by` clause, because then `ngOptions` will track the identity + * of the item not by reference, but by the result of the `track by` expression. For example, if your + * collection items have an id property, you would `track by item.id`. + * + * A different issue with objects or collections is that ngModel won't detect if an object property or + * a collection item changes. For that reason, `ngOptions` additionally watches the model using + * `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute. + * This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection + * has not changed identity, but only a property on the object or an item in the collection changes. + * + * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection + * if the model is an array). This means that changing a property deeper than the first level inside the + * object/collection will not trigger a re-rendering. + * + * ## `select` **`as`** + * + * Using `select` **`as`** will bind the result of the `select` expression to the model, but + * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources) + * or property name (for object data sources) of the value within the collection. If a **`track by`** expression + * is used, the result of that expression will be set as the value of the `option` and `select` elements. + * + * + * ### `select` **`as`** and **`track by`** + * + * <div class="alert alert-warning"> + * Be careful when using `select` **`as`** and **`track by`** in the same expression. + * </div> + * + * Given this array of items on the $scope: + * + * ```js + * $scope.items = [{ + * id: 1, + * label: 'aLabel', + * subItem: { name: 'aSubItem' } + * }, { + * id: 2, + * label: 'bLabel', + * subItem: { name: 'bSubItem' } + * }]; + * ``` + * + * This will work: + * + * ```html + * <select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select> + * ``` + * ```js + * $scope.selected = $scope.items[0]; + * ``` + * + * but this will not work: + * + * ```html + * <select ng-options="item.subItem as item.label for item in items track by item.id" ng-model="selected"></select> + * ``` + * ```js + * $scope.selected = $scope.items[0].subItem; + * ``` + * + * In both examples, the **`track by`** expression is applied successfully to each `item` in the + * `items` array. Because the selected option has been set programmatically in the controller, the + * **`track by`** expression is also applied to the `ngModel` value. In the first example, the + * `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with + * no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`** + * expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value + * is not matched against any `<option>` and the `<select>` appears as having no selected value. + * + * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} required The control is considered valid only if value is entered. + * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to + * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of + * `required` when you want to data-bind to the `required` attribute. + * @param {comprehension_expression=} ngOptions in one of the following forms: + * + * * for array data sources: + * * `label` **`for`** `value` **`in`** `array` + * * `select` **`as`** `label` **`for`** `value` **`in`** `array` + * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` + * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` + * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` + * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` + * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr` + * (for including a filter with `track by`) + * * for object data sources: + * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` + * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` + * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` + * * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object` + * * `select` **`as`** `label` **`group by`** `group` + * **`for` `(`**`key`**`,`** `value`**`) in`** `object` + * * `select` **`as`** `label` **`disable when`** `disable` + * **`for` `(`**`key`**`,`** `value`**`) in`** `object` + * + * Where: + * + * * `array` / `object`: an expression which evaluates to an array / object to iterate over. + * * `value`: local variable which will refer to each item in the `array` or each property value + * of `object` during iteration. + * * `key`: local variable which will refer to a property name in `object` during iteration. + * * `label`: The result of this expression will be the label for `<option>` element. The + * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). + * * `select`: The result of this expression will be bound to the model of the parent `<select>` + * element. If not specified, `select` expression will default to `value`. + * * `group`: The result of this expression will be used to group options using the `<optgroup>` + * DOM element. + * * `disable`: The result of this expression will be used to disable the rendered `<option>` + * element. Return `true` to disable. + * * `trackexpr`: Used when working with an array of objects. The result of this expression will be + * used to identify the objects in the array. The `trackexpr` will most likely refer to the + * `value` variable (e.g. `value.propertyName`). With this the selection is preserved + * even when the options are recreated (e.g. reloaded from the server). + * + * @example + <example module="selectExample"> + <file name="index.html"> + <script> + angular.module('selectExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.colors = [ + {name:'black', shade:'dark'}, + {name:'white', shade:'light', notAnOption: true}, + {name:'red', shade:'dark'}, + {name:'blue', shade:'dark', notAnOption: true}, + {name:'yellow', shade:'light', notAnOption: false} + ]; + $scope.myColor = $scope.colors[2]; // red + }]); + </script> + <div ng-controller="ExampleController"> + <ul> + <li ng-repeat="color in colors"> + <label>Name: <input ng-model="color.name"></label> + <label><input type="checkbox" ng-model="color.notAnOption"> Disabled?</label> + <button ng-click="colors.splice($index, 1)" aria-label="Remove">X</button> + </li> + <li> + <button ng-click="colors.push({})">add</button> + </li> + </ul> + <hr/> + <label>Color (null not allowed): + <select ng-model="myColor" ng-options="color.name for color in colors"></select> + </label><br/> + <label>Color (null allowed): + <span class="nullable"> + <select ng-model="myColor" ng-options="color.name for color in colors"> + <option value="">-- choose color --</option> + </select> + </span></label><br/> + + <label>Color grouped by shade: + <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors"> + </select> + </label><br/> + + <label>Color grouped by shade, with some disabled: + <select ng-model="myColor" + ng-options="color.name group by color.shade disable when color.notAnOption for color in colors"> + </select> + </label><br/> + + + + Select <button ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</button>. + <br/> + <hr/> + Currently selected: {{ {selected_color:myColor} }} + <div style="border:solid 1px black; height:20px" + ng-style="{'background-color':myColor.name}"> + </div> + </div> + </file> + <file name="protractor.js" type="protractor"> + it('should check ng-options', function() { + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); + element.all(by.model('myColor')).first().click(); + element.all(by.css('select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); + element(by.css('.nullable select[ng-model="myColor"]')).click(); + element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); + expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); + }); + </file> + </example> + */ + +// jshint maxlen: false +// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999 +var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/; + // 1: value expression (valueFn) + // 2: label expression (displayFn) + // 3: group by expression (groupByFn) + // 4: disable when expression (disableWhenFn) + // 5: array item variable name + // 6: object item key variable name + // 7: object item value variable name + // 8: collection expression + // 9: track by expression +// jshint maxlen: 100 + + +var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { + + function parseOptionsExpression(optionsExp, selectElement, scope) { + + var match = optionsExp.match(NG_OPTIONS_REGEXP); + if (!(match)) { + throw ngOptionsMinErr('iexp', + "Expected expression in form of " + + "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + + " but got '{0}'. Element: {1}", + optionsExp, startingTag(selectElement)); + } + + // Extract the parts from the ngOptions expression + + // The variable name for the value of the item in the collection + var valueName = match[5] || match[7]; + // The variable name for the key of the item in the collection + var keyName = match[6]; + + // An expression that generates the viewValue for an option if there is a label expression + var selectAs = / as /.test(match[0]) && match[1]; + // An expression that is used to track the id of each object in the options collection + var trackBy = match[9]; + // An expression that generates the viewValue for an option if there is no label expression + var valueFn = $parse(match[2] ? match[1] : valueName); + var selectAsFn = selectAs && $parse(selectAs); + var viewValueFn = selectAsFn || valueFn; + var trackByFn = trackBy && $parse(trackBy); + + // Get the value by which we are going to track the option + // if we have a trackFn then use that (passing scope and locals) + // otherwise just hash the given viewValue + var getTrackByValueFn = trackBy ? + function(value, locals) { return trackByFn(scope, locals); } : + function getHashOfValue(value) { return hashKey(value); }; + var getTrackByValue = function(value, key) { + return getTrackByValueFn(value, getLocals(value, key)); + }; + + var displayFn = $parse(match[2] || match[1]); + var groupByFn = $parse(match[3] || ''); + var disableWhenFn = $parse(match[4] || ''); + var valuesFn = $parse(match[8]); + + var locals = {}; + var getLocals = keyName ? function(value, key) { + locals[keyName] = key; + locals[valueName] = value; + return locals; + } : function(value) { + locals[valueName] = value; + return locals; + }; + + + function Option(selectValue, viewValue, label, group, disabled) { + this.selectValue = selectValue; + this.viewValue = viewValue; + this.label = label; + this.group = group; + this.disabled = disabled; + } + + function getOptionValuesKeys(optionValues) { + var optionValuesKeys; + + if (!keyName && isArrayLike(optionValues)) { + optionValuesKeys = optionValues; + } else { + // if object, extract keys, in enumeration order, unsorted + optionValuesKeys = []; + for (var itemKey in optionValues) { + if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') { + optionValuesKeys.push(itemKey); + } + } + } + return optionValuesKeys; + } + + return { + trackBy: trackBy, + getTrackByValue: getTrackByValue, + getWatchables: $parse(valuesFn, function(optionValues) { + // Create a collection of things that we would like to watch (watchedArray) + // so that they can all be watched using a single $watchCollection + // that only runs the handler once if anything changes + var watchedArray = []; + optionValues = optionValues || []; + + var optionValuesKeys = getOptionValuesKeys(optionValues); + var optionValuesLength = optionValuesKeys.length; + for (var index = 0; index < optionValuesLength; index++) { + var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; + var value = optionValues[key]; + + var locals = getLocals(optionValues[key], key); + var selectValue = getTrackByValueFn(optionValues[key], locals); + watchedArray.push(selectValue); + + // Only need to watch the displayFn if there is a specific label expression + if (match[2] || match[1]) { + var label = displayFn(scope, locals); + watchedArray.push(label); + } + + // Only need to watch the disableWhenFn if there is a specific disable expression + if (match[4]) { + var disableWhen = disableWhenFn(scope, locals); + watchedArray.push(disableWhen); + } + } + return watchedArray; + }), + + getOptions: function() { + + var optionItems = []; + var selectValueMap = {}; + + // The option values were already computed in the `getWatchables` fn, + // which must have been called to trigger `getOptions` + var optionValues = valuesFn(scope) || []; + var optionValuesKeys = getOptionValuesKeys(optionValues); + var optionValuesLength = optionValuesKeys.length; + + for (var index = 0; index < optionValuesLength; index++) { + var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; + var value = optionValues[key]; + var locals = getLocals(value, key); + var viewValue = viewValueFn(scope, locals); + var selectValue = getTrackByValueFn(viewValue, locals); + var label = displayFn(scope, locals); + var group = groupByFn(scope, locals); + var disabled = disableWhenFn(scope, locals); + var optionItem = new Option(selectValue, viewValue, label, group, disabled); + + optionItems.push(optionItem); + selectValueMap[selectValue] = optionItem; + } + + return { + items: optionItems, + selectValueMap: selectValueMap, + getOptionFromViewValue: function(value) { + return selectValueMap[getTrackByValue(value)]; + }, + getViewValueFromOption: function(option) { + // If the viewValue could be an object that may be mutated by the application, + // we need to make a copy and not return the reference to the value on the option. + return trackBy ? angular.copy(option.viewValue) : option.viewValue; + } + }; + } + }; + } + + + // we can't just jqLite('<option>') since jqLite is not smart enough + // to create it in <select> and IE barfs otherwise. + var optionTemplate = document.createElement('option'), + optGroupTemplate = document.createElement('optgroup'); + + + function ngOptionsPostLink(scope, selectElement, attr, ctrls) { + + // if ngModel is not defined, we don't need to do anything + var ngModelCtrl = ctrls[1]; + if (!ngModelCtrl) return; + + var selectCtrl = ctrls[0]; + var multiple = attr.multiple; + + // The emptyOption allows the application developer to provide their own custom "empty" + // option when the viewValue does not match any of the option values. + var emptyOption; + for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) { + if (children[i].value === '') { + emptyOption = children.eq(i); + break; + } + } + + var providedEmptyOption = !!emptyOption; + + var unknownOption = jqLite(optionTemplate.cloneNode(false)); + unknownOption.val('?'); + + var options; + var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope); + + + var renderEmptyOption = function() { + if (!providedEmptyOption) { + selectElement.prepend(emptyOption); + } + selectElement.val(''); + emptyOption.prop('selected', true); // needed for IE + emptyOption.attr('selected', true); + }; + + var removeEmptyOption = function() { + if (!providedEmptyOption) { + emptyOption.remove(); + } + }; + + + var renderUnknownOption = function() { + selectElement.prepend(unknownOption); + selectElement.val('?'); + unknownOption.prop('selected', true); // needed for IE + unknownOption.attr('selected', true); + }; + + var removeUnknownOption = function() { + unknownOption.remove(); + }; + + // Update the controller methods for multiple selectable options + if (!multiple) { + + selectCtrl.writeValue = function writeNgOptionsValue(value) { + var option = options.getOptionFromViewValue(value); + + if (option && !option.disabled) { + // Don't update the option when it is already selected. + // For example, the browser will select the first option by default. In that case, + // most properties are set automatically - except the `selected` attribute, which we + // set always + + if (selectElement[0].value !== option.selectValue) { + removeUnknownOption(); + removeEmptyOption(); + + selectElement[0].value = option.selectValue; + option.element.selected = true; + } + + option.element.setAttribute('selected', 'selected'); + } else { + if (value === null || providedEmptyOption) { + removeUnknownOption(); + renderEmptyOption(); + } else { + removeEmptyOption(); + renderUnknownOption(); + } + } + }; + + selectCtrl.readValue = function readNgOptionsValue() { + + var selectedOption = options.selectValueMap[selectElement.val()]; + + if (selectedOption && !selectedOption.disabled) { + removeEmptyOption(); + removeUnknownOption(); + return options.getViewValueFromOption(selectedOption); + } + return null; + }; + + // If we are using `track by` then we must watch the tracked value on the model + // since ngModel only watches for object identity change + if (ngOptions.trackBy) { + scope.$watch( + function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); }, + function() { ngModelCtrl.$render(); } + ); + } + + } else { + + ngModelCtrl.$isEmpty = function(value) { + return !value || value.length === 0; + }; + + + selectCtrl.writeValue = function writeNgOptionsMultiple(value) { + options.items.forEach(function(option) { + option.element.selected = false; + }); + + if (value) { + value.forEach(function(item) { + var option = options.getOptionFromViewValue(item); + if (option && !option.disabled) option.element.selected = true; + }); + } + }; + + + selectCtrl.readValue = function readNgOptionsMultiple() { + var selectedValues = selectElement.val() || [], + selections = []; + + forEach(selectedValues, function(value) { + var option = options.selectValueMap[value]; + if (option && !option.disabled) selections.push(options.getViewValueFromOption(option)); + }); + + return selections; + }; + + // If we are using `track by` then we must watch these tracked values on the model + // since ngModel only watches for object identity change + if (ngOptions.trackBy) { + + scope.$watchCollection(function() { + if (isArray(ngModelCtrl.$viewValue)) { + return ngModelCtrl.$viewValue.map(function(value) { + return ngOptions.getTrackByValue(value); + }); + } + }, function() { + ngModelCtrl.$render(); + }); + + } + } + + + if (providedEmptyOption) { + + // we need to remove it before calling selectElement.empty() because otherwise IE will + // remove the label from the element. wtf? + emptyOption.remove(); + + // compile the element since there might be bindings in it + $compile(emptyOption)(scope); + + // remove the class, which is added automatically because we recompile the element and it + // becomes the compilation root + emptyOption.removeClass('ng-scope'); + } else { + emptyOption = jqLite(optionTemplate.cloneNode(false)); + } + + // We need to do this here to ensure that the options object is defined + // when we first hit it in writeNgOptionsValue + updateOptions(); + + // We will re-render the option elements if the option values or labels change + scope.$watchCollection(ngOptions.getWatchables, updateOptions); + + // ------------------------------------------------------------------ // + + + function updateOptionElement(option, element) { + option.element = element; + element.disabled = option.disabled; + // NOTE: The label must be set before the value, otherwise IE10/11/EDGE create unresponsive + // selects in certain circumstances when multiple selects are next to each other and display + // the option list in listbox style, i.e. the select is [multiple], or specifies a [size]. + // See https://github.com/angular/angular.js/issues/11314 for more info. + // This is unfortunately untestable with unit / e2e tests + if (option.label !== element.label) { + element.label = option.label; + element.textContent = option.label; + } + if (option.value !== element.value) element.value = option.selectValue; + } + + function addOrReuseElement(parent, current, type, templateElement) { + var element; + // Check whether we can reuse the next element + if (current && lowercase(current.nodeName) === type) { + // The next element is the right type so reuse it + element = current; + } else { + // The next element is not the right type so create a new one + element = templateElement.cloneNode(false); + if (!current) { + // There are no more elements so just append it to the select + parent.appendChild(element); + } else { + // The next element is not a group so insert the new one + parent.insertBefore(element, current); + } + } + return element; + } + + + function removeExcessElements(current) { + var next; + while (current) { + next = current.nextSibling; + jqLiteRemove(current); + current = next; + } + } + + + function skipEmptyAndUnknownOptions(current) { + var emptyOption_ = emptyOption && emptyOption[0]; + var unknownOption_ = unknownOption && unknownOption[0]; + + // We cannot rely on the extracted empty option being the same as the compiled empty option, + // because the compiled empty option might have been replaced by a comment because + // it had an "element" transclusion directive on it (such as ngIf) + if (emptyOption_ || unknownOption_) { + while (current && + (current === emptyOption_ || + current === unknownOption_ || + current.nodeType === NODE_TYPE_COMMENT || + (nodeName_(current) === 'option' && current.value === ''))) { + current = current.nextSibling; + } + } + return current; + } + + + function updateOptions() { + + var previousValue = options && selectCtrl.readValue(); + + options = ngOptions.getOptions(); + + var groupMap = {}; + var currentElement = selectElement[0].firstChild; + + // Ensure that the empty option is always there if it was explicitly provided + if (providedEmptyOption) { + selectElement.prepend(emptyOption); + } + + currentElement = skipEmptyAndUnknownOptions(currentElement); + + options.items.forEach(function updateOption(option) { + var group; + var groupElement; + var optionElement; + + if (option.group) { + + // This option is to live in a group + // See if we have already created this group + group = groupMap[option.group]; + + if (!group) { + + // We have not already created this group + groupElement = addOrReuseElement(selectElement[0], + currentElement, + 'optgroup', + optGroupTemplate); + // Move to the next element + currentElement = groupElement.nextSibling; + + // Update the label on the group element + groupElement.label = option.group; + + // Store it for use later + group = groupMap[option.group] = { + groupElement: groupElement, + currentOptionElement: groupElement.firstChild + }; + + } + + // So now we have a group for this option we add the option to the group + optionElement = addOrReuseElement(group.groupElement, + group.currentOptionElement, + 'option', + optionTemplate); + updateOptionElement(option, optionElement); + // Move to the next element + group.currentOptionElement = optionElement.nextSibling; + + } else { + + // This option is not in a group + optionElement = addOrReuseElement(selectElement[0], + currentElement, + 'option', + optionTemplate); + updateOptionElement(option, optionElement); + // Move to the next element + currentElement = optionElement.nextSibling; + } + }); + + + // Now remove all excess options and group + Object.keys(groupMap).forEach(function(key) { + removeExcessElements(groupMap[key].currentOptionElement); + }); + removeExcessElements(currentElement); + + ngModelCtrl.$render(); + + // Check to see if the value has changed due to the update to the options + if (!ngModelCtrl.$isEmpty(previousValue)) { + var nextValue = selectCtrl.readValue(); + var isNotPrimitive = ngOptions.trackBy || multiple; + if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) { + ngModelCtrl.$setViewValue(nextValue); + ngModelCtrl.$render(); + } + } + + } + } + + return { + restrict: 'A', + terminal: true, + require: ['select', '?ngModel'], + link: { + pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) { + // Deactivate the SelectController.register method to prevent + // option directives from accidentally registering themselves + // (and unwanted $destroy handlers etc.) + ctrls[0].registerOption = noop; + }, + post: ngOptionsPostLink + } + }; +}]; + /** * @ngdoc directive * @name ngPluralize @@ -24141,6 +27378,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder * for <span ng-non-bindable>{{numberExpression}}</span>. * + * If no rule is defined for a category, then an empty string is displayed and a warning is generated. + * Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`. + * * # Configuring ngPluralize with offset * The `offset` attribute allows further customization of pluralized text, which can result in * a better user experience. For example, instead of the message "4 people are viewing this document", @@ -24187,9 +27427,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); }]); </script> <div ng-controller="ExampleController"> - Person 1:<input type="text" ng-model="person1" value="Igor" /><br/> - Person 2:<input type="text" ng-model="person2" value="Misko" /><br/> - Number of People:<input type="text" ng-model="personCount" value="1" /><br/> + <label>Person 1:<input type="text" ng-model="person1" value="Igor" /></label><br/> + <label>Person 2:<input type="text" ng-model="person2" value="Misko" /></label><br/> + <label>Number of People:<input type="text" ng-model="personCount" value="1" /></label><br/> <!--- Example with simple pluralization rules for en locale ---> Without Offset: @@ -24259,12 +27499,11 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); </file> </example> */ -var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { +var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) { var BRACE = /{}/g, IS_WHEN = /^when(Minus)?(.+)$/; return { - restrict: 'EA', link: function(scope, element, attr) { var numberExp = attr.count, whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs @@ -24301,9 +27540,18 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp // If both `count` and `lastCount` are NaN, we don't need to re-register a watch. // In JS `NaN !== NaN`, so we have to exlicitly check. - if ((count !== lastCount) && !(countIsNaN && isNaN(lastCount))) { + if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) { watchRemover(); - watchRemover = scope.$watch(whensExpFns[count], updateElementText); + var whenExpFn = whensExpFns[count]; + if (isUndefined(whenExpFn)) { + if (newVal != null) { + $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp); + } + watchRemover = noop; + updateElementText(); + } else { + watchRemover = scope.$watch(whenExpFn, updateElementText); + } lastCount = count; } }); @@ -24318,6 +27566,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp /** * @ngdoc directive * @name ngRepeat + * @multiElement * * @description * The `ngRepeat` directive instantiates a template once per item from a collection. Each template @@ -24335,8 +27584,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). | * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). | * - * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. - * This may be useful when, for instance, nesting ngRepeats. + * <div class="alert alert-info"> + * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}. + * This may be useful when, for instance, nesting ngRepeats. + * </div> + * * * # Iterating over object properties * @@ -24347,31 +27599,38 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * <div ng-repeat="(key, value) in myObj"> ... </div> * ``` * - * You need to be aware that the JavaScript specification does not define what order - * it will return the keys for an object. In order to have a guaranteed deterministic order - * for the keys, Angular versions up to and including 1.3 **sort the keys alphabetically**. + * You need to be aware that the JavaScript specification does not define the order of keys + * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive + * used to sort the keys alphabetically.) + * + * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser + * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing + * keys in the order in which they were defined, although there are exceptions when keys are deleted + * and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes). * * If this is not desired, the recommended workaround is to convert your object into an array * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter) * or implement a `$watch` on the object yourself. * - * In version 1.4 we will remove the sorting, since it seems that browsers generally follow the - * strategy of providing keys in the order in which they were defined, although there are exceptions - * when keys are deleted and reinstated. - * * * # Tracking and Duplicates * - * When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM: + * `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in + * the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM: * * * When an item is added, a new instance of the template is added to the DOM. * * When an item is removed, its template instance is removed from the DOM. * * When items are reordered, their respective templates are reordered in the DOM. * - * By default, `ngRepeat` does not allow duplicate items in arrays. This is because when - * there are duplicates, it is not possible to maintain a one-to-one mapping between collection - * items and DOM elements. + * To minimize creation of DOM elements, `ngRepeat` uses a function + * to "keep track" of all items in the collection and their corresponding DOM elements. + * For example, if an item is added to the collection, ngRepeat will know that all other items + * already have DOM elements, and will not re-render them. + * + * The default tracking function (which tracks items by their identity) does not allow + * duplicate items in arrays. This is because when there are duplicates, it is not possible + * to maintain a one-to-one mapping between collection items and DOM elements. * * If you do need to repeat duplicate items, you can substitute the default tracking behavior * with your own using the `track by` expression. @@ -24384,7 +27643,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * </div> * ``` * - * You may use arbitrary expressions in `track by`, including references to custom functions + * You may also use arbitrary expressions in `track by`, including references to custom functions * on the scope: * ```html * <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)"> @@ -24392,10 +27651,14 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * </div> * ``` * - * If you are working with objects that have an identifier property, you can track + * <div class="alert alert-success"> + * If you are working with objects that have an identifier property, you should track * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat` * will not have to rebuild the DOM elements for items it has already rendered, even if the - * JavaScript objects in the collection have been substituted for new ones: + * JavaScript objects in the collection have been substituted for new ones. For large collections, + * this signifincantly improves rendering performance. If you don't have a unique identifier, + * `track by $index` can also provide a performance boost. + * </div> * ```html * <div ng-repeat="model in collection track by model.id"> * {{model.name}} @@ -24410,6 +27673,15 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * </div> * ``` * + * <div class="alert alert-warning"> + * **Note:** `track by` must always be the last expression: + * </div> + * ``` + * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id"> + * {{model.name}} + * </div> + * ``` + * * # Special repeat start and end points * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively. @@ -24481,8 +27753,9 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression * is specified, ng-repeat associates elements by identity. It is an error to have * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are - * mapped to the same DOM element, which is not possible.) If filters are used in the expression, they should be - * applied before the tracking expression. + * mapped to the same DOM element, which is not possible.) + * + * Note that the tracking expression must come last, after any filters, and the alias expression. * * For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements * will be associated by item identity in the array. @@ -24506,6 +27779,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after * the items have been processed through the filter. * + * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end + * (and not as operator, inside an expression). + * + * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` . + * * @example * This example initializes the scope to a list of names and * then uses `ngRepeat` to display every person: @@ -24524,7 +27802,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp {name:'Samantha', age:60, gender:'girl'} ]"> I have {{friends.length}} friends. They are: - <input type="search" ng-model="q" placeholder="filter friends..." /> + <input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" /> <ul class="example-animate-container"> <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. @@ -24553,7 +27831,6 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp .animate-repeat.ng-move, .animate-repeat.ng-enter, .animate-repeat.ng-leave { - -webkit-transition:all linear 0.5s; transition:all linear 0.5s; } @@ -24722,14 +27999,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { trackByIdFn = trackByIdExpFn || trackByIdArrayFn; } else { trackByIdFn = trackByIdExpFn || trackByIdObjFn; - // if object, extract keys, sort them and use to determine order of iteration over obj props + // if object, extract keys, in enumeration order, unsorted collectionKeys = []; for (var itemKey in collection) { - if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') { + if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { collectionKeys.push(itemKey); } } - collectionKeys.sort(); } collectionLength = collectionKeys.length; @@ -24795,7 +28071,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); + $animate.move(getBlockNodes(block.clone), null, previousNode); } previousNode = getBlockEnd(block); updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); @@ -24807,8 +28083,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var endNode = ngRepeatEndComment.cloneNode(false); clone[clone.length++] = endNode; - // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? - $animate.enter(clone, null, jqLite(previousNode)); + $animate.enter(clone, null, previousNode); previousNode = endNode; // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later @@ -24831,6 +28106,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; /** * @ngdoc directive * @name ngShow + * @multiElement * * @description * The `ngShow` directive shows or hides the given HTML element based on the expression @@ -24924,7 +28200,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * @example <example module="ngAnimate" deps="angular-animate.js" animations="true"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked"><br/> + Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/> <div> Show: <div class="check-element animate-show" ng-show="checked"> @@ -24950,9 +28226,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; background: white; } - .animate-show.ng-hide-add.ng-hide-add-active, - .animate-show.ng-hide-remove.ng-hide-remove-active { - -webkit-transition: all linear 0.5s; + .animate-show.ng-hide-add, .animate-show.ng-hide-remove { transition: all linear 0.5s; } @@ -25006,6 +28280,7 @@ var ngShowDirective = ['$animate', function($animate) { /** * @ngdoc directive * @name ngHide + * @multiElement * * @description * The `ngHide` directive shows or hides the given HTML element based on the expression @@ -25089,7 +28364,7 @@ var ngShowDirective = ['$animate', function($animate) { * @example <example module="ngAnimate" deps="angular-animate.js" animations="true"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked"><br/> + Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/> <div> Show: <div class="check-element animate-hide" ng-show="checked"> @@ -25108,7 +28383,6 @@ var ngShowDirective = ['$animate', function($animate) { </file> <file name="animations.css"> .animate-hide { - -webkit-transition: all linear 0.5s; transition: all linear 0.5s; line-height: 20px; opacity: 1; @@ -25307,7 +28581,6 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { } .animate-switch.ng-animate { - -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; position:absolute; @@ -25346,7 +28619,6 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { */ var ngSwitchDirective = ['$animate', function($animate) { return { - restrict: 'EA', require: 'ngSwitch', // asks for $scope to fool the BC controller module @@ -25455,8 +28727,8 @@ var ngSwitchDefaultDirective = ngDirective({ }]); </script> <div ng-controller="ExampleController"> - <input ng-model="title"> <br/> - <textarea ng-model="text"></textarea> <br/> + <input ng-model="title" aria-label="title"> <br/> + <textarea ng-model="text" aria-label="text"></textarea> <br/> <pane title="{{title}}">{{text}}</pane> </div> </file> @@ -25541,7 +28813,153 @@ var scriptDirective = ['$templateCache', function($templateCache) { }; }]; -var ngOptionsMinErr = minErr('ngOptions'); +var noopNgModelController = { $setViewValue: noop, $render: noop }; + +function chromeHack(optionElement) { + // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 + // Adding an <option selected="selected"> element to a <select required="required"> should + // automatically select the new element + if (optionElement[0].hasAttribute('selected')) { + optionElement[0].selected = true; + } +} + +/** + * @ngdoc type + * @name select.SelectController + * @description + * The controller for the `<select>` directive. This provides support for reading + * and writing the selected value(s) of the control and also coordinates dynamically + * added `<option>` elements, perhaps by an `ngRepeat` directive. + */ +var SelectController = + ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + + var self = this, + optionsMap = new HashMap(); + + // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors + self.ngModelCtrl = noopNgModelController; + + // The "unknown" option is one that is prepended to the list if the viewValue + // does not match any of the options. When it is rendered the value of the unknown + // option is '? XXX ?' where XXX is the hashKey of the value that is not known. + // + // We can't just jqLite('<option>') since jqLite is not smart enough + // to create it in <select> and IE barfs otherwise. + self.unknownOption = jqLite(document.createElement('option')); + self.renderUnknownOption = function(val) { + var unknownVal = '? ' + hashKey(val) + ' ?'; + self.unknownOption.val(unknownVal); + $element.prepend(self.unknownOption); + $element.val(unknownVal); + }; + + $scope.$on('$destroy', function() { + // disable unknown option so that we don't do work when the whole select is being destroyed + self.renderUnknownOption = noop; + }); + + self.removeUnknownOption = function() { + if (self.unknownOption.parent()) self.unknownOption.remove(); + }; + + + // Read the value of the select control, the implementation of this changes depending + // upon whether the select can have multiple values and whether ngOptions is at work. + self.readValue = function readSingleValue() { + self.removeUnknownOption(); + return $element.val(); + }; + + + // Write the value to the select control, the implementation of this changes depending + // upon whether the select can have multiple values and whether ngOptions is at work. + self.writeValue = function writeSingleValue(value) { + if (self.hasOption(value)) { + self.removeUnknownOption(); + $element.val(value); + if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy + } else { + if (value == null && self.emptyOption) { + self.removeUnknownOption(); + $element.val(''); + } else { + self.renderUnknownOption(value); + } + } + }; + + + // Tell the select control that an option, with the given value, has been added + self.addOption = function(value, element) { + // Skip comment nodes, as they only pollute the `optionsMap` + if (element[0].nodeType === NODE_TYPE_COMMENT) return; + + assertNotHasOwnProperty(value, '"option value"'); + if (value === '') { + self.emptyOption = element; + } + var count = optionsMap.get(value) || 0; + optionsMap.put(value, count + 1); + self.ngModelCtrl.$render(); + chromeHack(element); + }; + + // Tell the select control that an option, with the given value, has been removed + self.removeOption = function(value) { + var count = optionsMap.get(value); + if (count) { + if (count === 1) { + optionsMap.remove(value); + if (value === '') { + self.emptyOption = undefined; + } + } else { + optionsMap.put(value, count - 1); + } + } + }; + + // Check whether the select control has an option matching the given value + self.hasOption = function(value) { + return !!optionsMap.get(value); + }; + + + self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) { + + if (interpolateValueFn) { + // The value attribute is interpolated + var oldVal; + optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { + if (isDefined(oldVal)) { + self.removeOption(oldVal); + } + oldVal = newVal; + self.addOption(newVal, optionElement); + }); + } else if (interpolateTextFn) { + // The text content is interpolated + optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) { + optionAttrs.$set('value', newVal); + if (oldVal !== newVal) { + self.removeOption(oldVal); + } + self.addOption(newVal, optionElement); + }); + } else { + // The value attribute is static + self.addOption(optionAttrs.value, optionElement); + } + + optionElement.on('$destroy', function() { + self.removeOption(optionAttrs.value); + self.ngModelCtrl.$render(); + }); + }; +}]; + /** * @ngdoc directive * @name select @@ -25550,767 +28968,329 @@ var ngOptionsMinErr = minErr('ngOptions'); * @description * HTML `SELECT` element with angular data-binding. * - * # `ngOptions` + * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding + * between the scope and the `<select>` control (including setting default values). + * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or + * {@link ngOptions `ngOptions`} directives. * - * The `ngOptions` attribute can be used to dynamically generate a list of `<option>` - * elements for the `<select>` element using the array or object obtained by evaluating the - * `ngOptions` comprehension expression. + * When an item in the `<select>` menu is selected, the value of the selected option will be bound + * to the model identified by the `ngModel` directive. With static or repeated options, this is + * the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing. + * If you want dynamic value attributes, you can use interpolation inside the value attribute. * - * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a - * similar result. However, `ngOptions` provides some benefits such as reducing memory and - * increasing speed by not creating a new scope for each repeated instance, as well as providing - * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the - * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound - * to a non-string value. This is because an option element can only be bound to string values at - * present. + * <div class="alert alert-warning"> + * Note that the value of a `select` directive used without `ngOptions` is always a string. + * When the model needs to be bound to a non-string value, you must either explictly convert it + * using a directive (see example below) or use `ngOptions` to specify the set of options. + * This is because an option element can only be bound to string values at present. + * </div> * - * When an item in the `<select>` menu is selected, the array element or object property - * represented by the selected option will be bound to the model identified by the `ngModel` - * directive. + * If the viewValue of `ngModel` does not match any of the options, then the control + * will automatically add an "unknown" option, which it then removes when the mismatch is resolved. * * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can * be nested into the `<select>` element. This element will then represent the `null` or "not selected" * option. See example below for demonstration. * - * <div class="alert alert-warning"> - * **Note:** `ngModel` compares by reference, not value. This is important when binding to an - * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/). + * <div class="alert alert-info"> + * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions + * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as + * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression, and additionally in reducing memory and increasing speed by not creating + * a new scope for each repeated instance. * </div> * - * ## `select` **`as`** - * - * Using `select` **`as`** will bind the result of the `select` expression to the model, but - * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources) - * or property name (for object data sources) of the value within the collection. If a **`track by`** expression - * is used, the result of that expression will be set as the value of the `option` and `select` elements. * + * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string=} name Property name of the form under which the control is published. + * @param {string=} multiple Allows multiple options to be selected. The selected values will be + * bound to the model as an array. + * @param {string=} required Sets `required` validation error key if the value is not entered. + * @param {string=} ngRequired Adds required attribute and required validation constraint to + * the element when the ngRequired expression evaluates to true. Use ngRequired instead of required + * when you want to data-bind to the required attribute. + * @param {string=} ngChange Angular expression to be executed when selected option(s) changes due to user + * interaction with the select element. + * @param {string=} ngOptions sets the options that the select is populated with and defines what is + * set on the model on selection. See {@link ngOptions `ngOptions`}. * - * ### `select` **`as`** and **`track by`** + * @example + * ### Simple `select` elements with static options * - * <div class="alert alert-warning"> - * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together. + * <example name="static-select" module="staticSelect"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="myForm"> + * <label for="singleSelect"> Single select: </label><br> + * <select name="singleSelect" ng-model="data.singleSelect"> + * <option value="option-1">Option 1</option> + * <option value="option-2">Option 2</option> + * </select><br> + * + * <label for="singleSelect"> Single select with "not selected" option and dynamic option values: </label><br> + * <select name="singleSelect" id="singleSelect" ng-model="data.singleSelect"> + * <option value="">---Please select---</option> <!-- not selected / blank option --> + * <option value="{{data.option1}}">Option 1</option> <!-- interpolation --> + * <option value="option-2">Option 2</option> + * </select><br> + * <button ng-click="forceUnknownOption()">Force unknown option</button><br> + * <tt>singleSelect = {{data.singleSelect}}</tt> + * + * <hr> + * <label for="multipleSelect"> Multiple select: </label><br> + * <select name="multipleSelect" id="multipleSelect" ng-model="data.multipleSelect" multiple> + * <option value="option-1">Option 1</option> + * <option value="option-2">Option 2</option> + * <option value="option-3">Option 3</option> + * </select><br> + * <tt>multipleSelect = {{data.multipleSelect}}</tt><br/> + * </form> * </div> + * </file> + * <file name="app.js"> + * angular.module('staticSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.data = { + * singleSelect: null, + * multipleSelect: [], + * option1: 'option-1', + * }; * - * Consider the following example: - * - * ```html - * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> - * ``` - * - * ```js - * $scope.values = [{ - * id: 1, - * label: 'aLabel', - * subItem: { name: 'aSubItem' } - * }, { - * id: 2, - * label: 'bLabel', - * subItem: { name: 'bSubItem' } - * }]; - * - * $scope.selected = { name: 'aSubItem' }; - * ``` + * $scope.forceUnknownOption = function() { + * $scope.data.singleSelect = 'nonsense'; + * }; + * }]); + * </file> + *</example> * - * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element - * of the data source (to `item` in this example). To calculate whether an element is selected, we do the - * following: + * ### Using `ngRepeat` to generate `select` options + * <example name="ngrepeat-select" module="ngrepeatSelect"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="myForm"> + * <label for="repeatSelect"> Repeat select: </label> + * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect"> + * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option> + * </select> + * </form> + * <hr> + * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/> + * </div> + * </file> + * <file name="app.js"> + * angular.module('ngrepeatSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.data = { + * repeatSelect: null, + * availableOptions: [ + * {id: '1', name: 'Option A'}, + * {id: '2', name: 'Option B'}, + * {id: '3', name: 'Option C'} + * ], + * }; + * }]); + * </file> + *</example> * - * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]` - * 2. Apply **`track by`** to the already selected value in `ngModel`. - * In the example: this is not possible as **`track by`** refers to `item.id`, but the selected - * value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to - * a wrong object, the selected element can't be found, `<select>` is always reset to the "not - * selected" option. * + * ### Using `select` with `ngOptions` and setting a default value + * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples. * - * @param {string} ngModel Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the control is published. - * @param {string=} required The control is considered valid only if value is entered. - * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to - * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of - * `required` when you want to data-bind to the `required` attribute. - * @param {comprehension_expression=} ngOptions in one of the following forms: + * <example name="select-with-default-values" module="defaultValueSelect"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="myForm"> + * <label for="mySelect">Make a choice:</label> + * <select name="mySelect" id="mySelect" + * ng-options="option.name for option in data.availableOptions track by option.id" + * ng-model="data.selectedOption"></select> + * </form> + * <hr> + * <tt>option = {{data.selectedOption}}</tt><br/> + * </div> + * </file> + * <file name="app.js"> + * angular.module('defaultValueSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.data = { + * availableOptions: [ + * {id: '1', name: 'Option A'}, + * {id: '2', name: 'Option B'}, + * {id: '3', name: 'Option C'} + * ], + * selectedOption: {id: '3', name: 'Option C'} //This sets the default value of the select in the ui + * }; + * }]); + * </file> + *</example> * - * * for array data sources: - * * `label` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` - * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr` - * (for including a filter with `track by`) - * * for object data sources: - * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`group by`** `group` - * **`for` `(`**`key`**`,`** `value`**`) in`** `object` * - * Where: + * ### Binding `select` to a non-string value via `ngModel` parsing / formatting * - * * `array` / `object`: an expression which evaluates to an array / object to iterate over. - * * `value`: local variable which will refer to each item in the `array` or each property value - * of `object` during iteration. - * * `key`: local variable which will refer to a property name in `object` during iteration. - * * `label`: The result of this expression will be the label for `<option>` element. The - * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). - * * `select`: The result of this expression will be bound to the model of the parent `<select>` - * element. If not specified, `select` expression will default to `value`. - * * `group`: The result of this expression will be used to group options using the `<optgroup>` - * DOM element. - * * `trackexpr`: Used when working with an array of objects. The result of this expression will be - * used to identify the objects in the array. The `trackexpr` will most likely refer to the - * `value` variable (e.g. `value.propertyName`). With this the selection is preserved - * even when the options are recreated (e.g. reloaded from the server). + * <example name="select-with-non-string-options" module="nonStringSelect"> + * <file name="index.html"> + * <select ng-model="model.id" convert-to-number> + * <option value="0">Zero</option> + * <option value="1">One</option> + * <option value="2">Two</option> + * </select> + * {{ model }} + * </file> + * <file name="app.js"> + * angular.module('nonStringSelect', []) + * .run(function($rootScope) { + * $rootScope.model = { id: 2 }; + * }) + * .directive('convertToNumber', function() { + * return { + * require: 'ngModel', + * link: function(scope, element, attrs, ngModel) { + * ngModel.$parsers.push(function(val) { + * return parseInt(val, 10); + * }); + * ngModel.$formatters.push(function(val) { + * return '' + val; + * }); + * } + * }; + * }); + * </file> + * <file name="protractor.js" type="protractor"> + * it('should initialize to model', function() { + * var select = element(by.css('select')); + * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two'); + * }); + * </file> + * </example> * - * @example - <example module="selectExample"> - <file name="index.html"> - <script> - angular.module('selectExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - $scope.myColor = $scope.colors[2]; // red - }]); - </script> - <div ng-controller="ExampleController"> - <ul> - <li ng-repeat="color in colors"> - Name: <input ng-model="color.name"> - [<a href ng-click="colors.splice($index, 1)">X</a>] - </li> - <li> - [<a href ng-click="colors.push({})">add</a>] - </li> - </ul> - <hr/> - Color (null not allowed): - <select ng-model="myColor" ng-options="color.name for color in colors"></select><br> - - Color (null allowed): - <span class="nullable"> - <select ng-model="myColor" ng-options="color.name for color in colors"> - <option value="">-- choose color --</option> - </select> - </span><br/> - - Color grouped by shade: - <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors"> - </select><br/> - - - Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br> - <hr/> - Currently selected: {{ {selected_color:myColor} }} - <div style="border:solid 1px black; height:20px" - ng-style="{'background-color':myColor.name}"> - </div> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should check ng-options', function() { - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red'); - element.all(by.model('myColor')).first().click(); - element.all(by.css('select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black'); - element(by.css('.nullable select[ng-model="myColor"]')).click(); - element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click(); - expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null'); - }); - </file> - </example> */ - -var ngOptionsDirective = valueFn({ - restrict: 'A', - terminal: true -}); - -// jshint maxlen: false -var selectDirective = ['$compile', '$parse', function($compile, $parse) { - //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 - var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/, - nullModelCtrl = {$setViewValue: noop}; -// jshint maxlen: 100 +var selectDirective = function() { return { restrict: 'E', require: ['select', '?ngModel'], - controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { - var self = this, - optionsMap = {}, - ngModelCtrl = nullModelCtrl, - nullOption, - unknownOption; - - - self.databound = $attrs.ngModel; - - - self.init = function(ngModelCtrl_, nullOption_, unknownOption_) { - ngModelCtrl = ngModelCtrl_; - nullOption = nullOption_; - unknownOption = unknownOption_; - }; - - - self.addOption = function(value, element) { - assertNotHasOwnProperty(value, '"option value"'); - optionsMap[value] = true; - - if (ngModelCtrl.$viewValue == value) { - $element.val(value); - if (unknownOption.parent()) unknownOption.remove(); - } - // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459 - // Adding an <option selected="selected"> element to a <select required="required"> should - // automatically select the new element - if (element && element[0].hasAttribute('selected')) { - element[0].selected = true; - } - }; - - - self.removeOption = function(value) { - if (this.hasOption(value)) { - delete optionsMap[value]; - if (ngModelCtrl.$viewValue === value) { - this.renderUnknownOption(value); - } - } - }; - - - self.renderUnknownOption = function(val) { - var unknownVal = '? ' + hashKey(val) + ' ?'; - unknownOption.val(unknownVal); - $element.prepend(unknownOption); - $element.val(unknownVal); - unknownOption.prop('selected', true); // needed for IE - }; - - - self.hasOption = function(value) { - return optionsMap.hasOwnProperty(value); - }; + controller: SelectController, + priority: 1, + link: { + pre: selectPreLink, + post: selectPostLink + } + }; - $scope.$on('$destroy', function() { - // disable unknown option so that we don't do work when the whole select is being destroyed - self.renderUnknownOption = noop; - }); - }], + function selectPreLink(scope, element, attr, ctrls) { - link: function(scope, element, attr, ctrls) { // if ngModel is not defined, we don't need to do anything - if (!ctrls[1]) return; - - var selectCtrl = ctrls[0], - ngModelCtrl = ctrls[1], - multiple = attr.multiple, - optionsExp = attr.ngOptions, - nullOption = false, // if false, user will not be able to select it (used by ngOptions) - emptyOption, - renderScheduled = false, - // we can't just jqLite('<option>') since jqLite is not smart enough - // to create it in <select> and IE barfs otherwise. - optionTemplate = jqLite(document.createElement('option')), - optGroupTemplate =jqLite(document.createElement('optgroup')), - unknownOption = optionTemplate.clone(); - - // find "null" option - for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) { - if (children[i].value === '') { - emptyOption = nullOption = children.eq(i); - break; - } - } + var ngModelCtrl = ctrls[1]; + if (!ngModelCtrl) return; - selectCtrl.init(ngModelCtrl, nullOption, unknownOption); - - // required validator - if (multiple) { - ngModelCtrl.$isEmpty = function(value) { - return !value || value.length === 0; - }; - } - - if (optionsExp) setupAsOptions(scope, element, ngModelCtrl); - else if (multiple) setupAsMultiple(scope, element, ngModelCtrl); - else setupAsSingle(scope, element, ngModelCtrl, selectCtrl); + var selectCtrl = ctrls[0]; + selectCtrl.ngModelCtrl = ngModelCtrl; - //////////////////////////// - - - - function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) { - ngModelCtrl.$render = function() { - var viewValue = ngModelCtrl.$viewValue; + // When the selected item(s) changes we delegate getting the value of the select control + // to the `readValue` method, which can be changed if the select can have multiple + // selected values or if the options are being generated by `ngOptions` + element.on('change', function() { + scope.$apply(function() { + ngModelCtrl.$setViewValue(selectCtrl.readValue()); + }); + }); - if (selectCtrl.hasOption(viewValue)) { - if (unknownOption.parent()) unknownOption.remove(); - selectElement.val(viewValue); - if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy - } else { - if (viewValue == null && emptyOption) { - selectElement.val(''); - } else { - selectCtrl.renderUnknownOption(viewValue); + // If the select allows multiple values then we need to modify how we read and write + // values from and to the control; also what it means for the value to be empty and + // we have to add an extra watch since ngModel doesn't work well with arrays - it + // doesn't trigger rendering if only an item in the array changes. + if (attr.multiple) { + + // Read value now needs to check each option to see if it is selected + selectCtrl.readValue = function readMultipleValue() { + var array = []; + forEach(element.find('option'), function(option) { + if (option.selected) { + array.push(option.value); } - } - }; - - selectElement.on('change', function() { - scope.$apply(function() { - if (unknownOption.parent()) unknownOption.remove(); - ngModelCtrl.$setViewValue(selectElement.val()); }); - }); - } + return array; + }; - function setupAsMultiple(scope, selectElement, ctrl) { - var lastView; - ctrl.$render = function() { - var items = new HashMap(ctrl.$viewValue); - forEach(selectElement.find('option'), function(option) { + // Write value now needs to set the selected property of each matching option + selectCtrl.writeValue = function writeMultipleValue(value) { + var items = new HashMap(value); + forEach(element.find('option'), function(option) { option.selected = isDefined(items.get(option.value)); }); }; // we have to do it on each watch since ngModel watches reference, but // we need to work of an array, so we need to see if anything was inserted/removed + var lastView, lastViewRef = NaN; scope.$watch(function selectMultipleWatch() { - if (!equals(lastView, ctrl.$viewValue)) { - lastView = shallowCopy(ctrl.$viewValue); - ctrl.$render(); + if (lastViewRef === ngModelCtrl.$viewValue && !equals(lastView, ngModelCtrl.$viewValue)) { + lastView = shallowCopy(ngModelCtrl.$viewValue); + ngModelCtrl.$render(); } + lastViewRef = ngModelCtrl.$viewValue; }); - selectElement.on('change', function() { - scope.$apply(function() { - var array = []; - forEach(selectElement.find('option'), function(option) { - if (option.selected) { - array.push(option.value); - } - }); - ctrl.$setViewValue(array); - }); - }); - } - - function setupAsOptions(scope, selectElement, ctrl) { - var match; - - if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) { - throw ngOptionsMinErr('iexp', - "Expected expression in form of " + - "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '{0}'. Element: {1}", - optionsExp, startingTag(selectElement)); - } - - var displayFn = $parse(match[2] || match[1]), - valueName = match[4] || match[6], - selectAs = / as /.test(match[0]) && match[1], - selectAsFn = selectAs ? $parse(selectAs) : null, - keyName = match[5], - groupByFn = $parse(match[3] || ''), - valueFn = $parse(match[2] ? match[1] : valueName), - valuesFn = $parse(match[7]), - track = match[8], - trackFn = track ? $parse(match[8]) : null, - trackKeysCache = {}, - // This is an array of array of existing option groups in DOM. - // We try to reuse these if possible - // - optionGroupsCache[0] is the options with no option group - // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element - optionGroupsCache = [[{element: selectElement, label:''}]], - //re-usable object to represent option's locals - locals = {}; - - if (nullOption) { - // compile the element since there might be bindings in it - $compile(nullOption)(scope); - - // remove the class, which is added automatically because we recompile the element and it - // becomes the compilation root - nullOption.removeClass('ng-scope'); - - // we need to remove it before calling selectElement.empty() because otherwise IE will - // remove the label from the element. wtf? - nullOption.remove(); - } - - // clear contents, we'll add what's needed based on the model - selectElement.empty(); - - selectElement.on('change', selectionChanged); - - ctrl.$render = render; - - scope.$watchCollection(valuesFn, scheduleRendering); - scope.$watchCollection(getLabels, scheduleRendering); - - if (multiple) { - scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering); - } - - // ------------------------------------------------------------------ // - - function callExpression(exprFn, key, value) { - locals[valueName] = value; - if (keyName) locals[keyName] = key; - return exprFn(scope, locals); - } - - function selectionChanged() { - scope.$apply(function() { - var collection = valuesFn(scope) || []; - var viewValue; - if (multiple) { - viewValue = []; - forEach(selectElement.val(), function(selectedKey) { - selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey; - viewValue.push(getViewValue(selectedKey, collection[selectedKey])); - }); - } else { - var selectedKey = trackFn ? trackKeysCache[selectElement.val()] : selectElement.val(); - viewValue = getViewValue(selectedKey, collection[selectedKey]); - } - ctrl.$setViewValue(viewValue); - render(); - }); - } - - function getViewValue(key, value) { - if (key === '?') { - return undefined; - } else if (key === '') { - return null; - } else { - var viewValueFn = selectAsFn ? selectAsFn : valueFn; - return callExpression(viewValueFn, key, value); - } - } - - function getLabels() { - var values = valuesFn(scope); - var toDisplay; - if (values && isArray(values)) { - toDisplay = new Array(values.length); - for (var i = 0, ii = values.length; i < ii; i++) { - toDisplay[i] = callExpression(displayFn, i, values[i]); - } - return toDisplay; - } else if (values) { - // TODO: Add a test for this case - toDisplay = {}; - for (var prop in values) { - if (values.hasOwnProperty(prop)) { - toDisplay[prop] = callExpression(displayFn, prop, values[prop]); - } - } - } - return toDisplay; - } - - function createIsSelectedFn(viewValue) { - var selectedSet; - if (multiple) { - if (trackFn && isArray(viewValue)) { - - selectedSet = new HashMap([]); - for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) { - // tracking by key - selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true); - } - } else { - selectedSet = new HashMap(viewValue); - } - } else if (trackFn) { - viewValue = callExpression(trackFn, null, viewValue); - } - - return function isSelected(key, value) { - var compareValueFn; - if (trackFn) { - compareValueFn = trackFn; - } else if (selectAsFn) { - compareValueFn = selectAsFn; - } else { - compareValueFn = valueFn; - } - - if (multiple) { - return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value))); - } else { - return viewValue === callExpression(compareValueFn, key, value); - } - }; - } - - function scheduleRendering() { - if (!renderScheduled) { - scope.$$postDigest(render); - renderScheduled = true; - } - } - - /** - * A new labelMap is created with each render. - * This function is called for each existing option with added=false, - * and each new option with added=true. - * - Labels that are passed to this method twice, - * (once with added=true and once with added=false) will end up with a value of 0, and - * will cause no change to happen to the corresponding option. - * - Labels that are passed to this method only once with added=false will end up with a - * value of -1 and will eventually be passed to selectCtrl.removeOption() - * - Labels that are passed to this method only once with added=true will end up with a - * value of 1 and will eventually be passed to selectCtrl.addOption() - */ - function updateLabelMap(labelMap, label, added) { - labelMap[label] = labelMap[label] || 0; - labelMap[label] += (added ? 1 : -1); - } - - function render() { - renderScheduled = false; - - // Temporary location for the option groups before we render them - var optionGroups = {'':[]}, - optionGroupNames = [''], - optionGroupName, - optionGroup, - option, - existingParent, existingOptions, existingOption, - viewValue = ctrl.$viewValue, - values = valuesFn(scope) || [], - keys = keyName ? sortedKeys(values) : values, - key, - value, - groupLength, length, - groupIndex, index, - labelMap = {}, - selected, - isSelected = createIsSelectedFn(viewValue), - anySelected = false, - lastElement, - element, - label, - optionId; - - trackKeysCache = {}; - - // We now build up the list of options we need (we merge later) - for (index = 0; length = keys.length, index < length; index++) { - key = index; - if (keyName) { - key = keys[index]; - if (key.charAt(0) === '$') continue; - } - value = values[key]; - - optionGroupName = callExpression(groupByFn, key, value) || ''; - if (!(optionGroup = optionGroups[optionGroupName])) { - optionGroup = optionGroups[optionGroupName] = []; - optionGroupNames.push(optionGroupName); - } - - selected = isSelected(key, value); - anySelected = anySelected || selected; - - label = callExpression(displayFn, key, value); // what will be seen by the user - - // doing displayFn(scope, locals) || '' overwrites zero values - label = isDefined(label) ? label : ''; - optionId = trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index); - if (trackFn) { - trackKeysCache[optionId] = key; - } - - optionGroup.push({ - // either the index into array or key from object - id: optionId, - label: label, - selected: selected // determine if we should be selected - }); - } - if (!multiple) { - if (nullOption || viewValue === null) { - // insert null option if we have a placeholder, or the model is null - optionGroups[''].unshift({id:'', label:'', selected:!anySelected}); - } else if (!anySelected) { - // option could not be found, we have to insert the undefined item - optionGroups[''].unshift({id:'?', label:'', selected:true}); - } - } - - // Now we need to update the list of DOM nodes to match the optionGroups we computed above - for (groupIndex = 0, groupLength = optionGroupNames.length; - groupIndex < groupLength; - groupIndex++) { - // current option group name or '' if no group - optionGroupName = optionGroupNames[groupIndex]; - - // list of options for that group. (first item has the parent) - optionGroup = optionGroups[optionGroupName]; - - if (optionGroupsCache.length <= groupIndex) { - // we need to grow the optionGroups - existingParent = { - element: optGroupTemplate.clone().attr('label', optionGroupName), - label: optionGroup.label - }; - existingOptions = [existingParent]; - optionGroupsCache.push(existingOptions); - selectElement.append(existingParent.element); - } else { - existingOptions = optionGroupsCache[groupIndex]; - existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element - - // update the OPTGROUP label if not the same. - if (existingParent.label != optionGroupName) { - existingParent.element.attr('label', existingParent.label = optionGroupName); - } - } - - lastElement = null; // start at the beginning - for (index = 0, length = optionGroup.length; index < length; index++) { - option = optionGroup[index]; - if ((existingOption = existingOptions[index + 1])) { - // reuse elements - lastElement = existingOption.element; - if (existingOption.label !== option.label) { - updateLabelMap(labelMap, existingOption.label, false); - updateLabelMap(labelMap, option.label, true); - lastElement.text(existingOption.label = option.label); - lastElement.prop('label', existingOption.label); - } - if (existingOption.id !== option.id) { - lastElement.val(existingOption.id = option.id); - } - // lastElement.prop('selected') provided by jQuery has side-effects - if (lastElement[0].selected !== option.selected) { - lastElement.prop('selected', (existingOption.selected = option.selected)); - if (msie) { - // See #7692 - // The selected item wouldn't visually update on IE without this. - // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well - lastElement.prop('selected', existingOption.selected); - } - } - } else { - // grow elements - - // if it's a null option - if (option.id === '' && nullOption) { - // put back the pre-compiled element - element = nullOption; - } else { - // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but - // in this version of jQuery on some browser the .text() returns a string - // rather then the element. - (element = optionTemplate.clone()) - .val(option.id) - .prop('selected', option.selected) - .attr('selected', option.selected) - .prop('label', option.label) - .text(option.label); - } + // If we are a multiple select then value is now a collection + // so the meaning of $isEmpty changes + ngModelCtrl.$isEmpty = function(value) { + return !value || value.length === 0; + }; - existingOptions.push(existingOption = { - element: element, - label: option.label, - id: option.id, - selected: option.selected - }); - updateLabelMap(labelMap, option.label, true); - if (lastElement) { - lastElement.after(element); - } else { - existingParent.element.append(element); - } - lastElement = element; - } - } - // remove any excessive OPTIONs in a group - index++; // increment since the existingOptions[0] is parent element not OPTION - while (existingOptions.length > index) { - option = existingOptions.pop(); - updateLabelMap(labelMap, option.label, false); - option.element.remove(); - } - } - // remove any excessive OPTGROUPs from select - while (optionGroupsCache.length > groupIndex) { - // remove all the labels in the option group - optionGroup = optionGroupsCache.pop(); - for (index = 1; index < optionGroup.length; ++index) { - updateLabelMap(labelMap, optionGroup[index].label, false); - } - optionGroup[0].element.remove(); - } - forEach(labelMap, function(count, label) { - if (count > 0) { - selectCtrl.addOption(label); - } else if (count < 0) { - selectCtrl.removeOption(label); - } - }); - } } } - }; -}]; -var optionDirective = ['$interpolate', function($interpolate) { - var nullSelectCtrl = { - addOption: noop, - removeOption: noop - }; + function selectPostLink(scope, element, attrs, ctrls) { + // if ngModel is not defined, we don't need to do anything + var ngModelCtrl = ctrls[1]; + if (!ngModelCtrl) return; + + var selectCtrl = ctrls[0]; + + // We delegate rendering to the `writeValue` method, which can be changed + // if the select can have multiple selected values or if the options are being + // generated by `ngOptions`. + // This must be done in the postLink fn to prevent $render to be called before + // all nodes have been linked correctly. + ngModelCtrl.$render = function() { + selectCtrl.writeValue(ngModelCtrl.$viewValue); + }; + } +}; + +// The option directive is purely designed to communicate the existence (or lack of) +// of dynamically created (and destroyed) option elements to their containing select +// directive via its controller. +var optionDirective = ['$interpolate', function($interpolate) { return { restrict: 'E', priority: 100, compile: function(element, attr) { - if (isUndefined(attr.value)) { - var interpolateFn = $interpolate(element.text(), true); - if (!interpolateFn) { + if (isDefined(attr.value)) { + // If the value attribute is defined, check if it contains an interpolation + var interpolateValueFn = $interpolate(attr.value, true); + } else { + // If the value attribute is not defined then we fall back to the + // text content of the option element, which may be interpolated + var interpolateTextFn = $interpolate(element.text(), true); + if (!interpolateTextFn) { attr.$set('value', element.text()); } } return function(scope, element, attr) { + // This is an optimization over using ^^ since we don't want to have to search + // all the way to the root of the DOM for every single option element var selectCtrlName = '$selectController', parent = element.parent(), selectCtrl = parent.data(selectCtrlName) || parent.parent().data(selectCtrlName); // in case we are in optgroup - if (!selectCtrl || !selectCtrl.databound) { - selectCtrl = nullSelectCtrl; + if (selectCtrl) { + selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn); } - - if (interpolateFn) { - scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { - attr.$set('value', newVal); - if (oldVal !== newVal) { - selectCtrl.removeOption(oldVal); - } - selectCtrl.addOption(newVal, element); - }); - } else { - selectCtrl.addOption(attr.value, element); - } - - element.on('$destroy', function() { - selectCtrl.removeOption(attr.value); - }); }; } }; @@ -26321,6 +29301,64 @@ var styleDirective = valueFn({ terminal: false }); +/** + * @ngdoc directive + * @name ngRequired + * + * @description + * + * ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be + * applied to custom controls. + * + * The directive sets the `required` attribute on the element if the Angular expression inside + * `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we + * cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide} + * for more info. + * + * The validator will set the `required` error key to true if the `required` attribute is set and + * calling {@link ngModel.NgModelController#$isEmpty `NgModelController.$isEmpty`} with the + * {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} returns `true`. For example, the + * `$isEmpty()` implementation for `input[text]` checks the length of the `$viewValue`. When developing + * custom controls, `$isEmpty()` can be overwritten to account for a $viewValue that is not string-based. + * + * @example + * <example name="ngRequiredDirective" module="ngRequiredExample"> + * <file name="index.html"> + * <script> + * angular.module('ngRequiredExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.required = true; + * }]); + * </script> + * <div ng-controller="ExampleController"> + * <form name="form"> + * <label for="required">Toggle required: </label> + * <input type="checkbox" ng-model="required" id="required" /> + * <br> + * <label for="input">This input must be filled if `required` is true: </label> + * <input type="text" ng-model="model" id="input" name="input" ng-required="required" /><br> + * <hr> + * required error set? = <code>{{form.input.$error.required}}</code><br> + * model = <code>{{model}}</code> + * </form> + * </div> + * </file> + * <file name="protractor.js" type="protractor"> + var required = element(by.binding('form.input.$error.required')); + var model = element(by.binding('model')); + var input = element(by.id('input')); + + it('should set the required error', function() { + expect(required.getText()).toContain('true'); + + input.sendKeys('123'); + expect(required.getText()).not.toContain('true'); + expect(model.getText()).toContain('123'); + }); + * </file> + * </example> + */ var requiredDirective = function() { return { restrict: 'A', @@ -26340,7 +29378,81 @@ var requiredDirective = function() { }; }; +/** + * @ngdoc directive + * @name ngPattern + * + * @description + * + * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * does not match a RegExp which is obtained by evaluating the Angular expression given in the + * `ngPattern` attribute value: + * * If the expression evaluates to a RegExp object, then this is used directly. + * * If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it + * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * + * <div class="alert alert-info"> + * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + * </div> + * + * <div class="alert alert-info"> + * **Note:** This directive is also added when the plain `pattern` attribute is used, with two + * differences: + * <ol> + * <li> + * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is + * not available. + * </li> + * <li> + * The `ngPattern` attribute must be an expression, while the `pattern` value must be + * interpolated. + * </li> + * </ol> + * </div> + * + * @example + * <example name="ngPatternDirective" module="ngPatternExample"> + * <file name="index.html"> + * <script> + * angular.module('ngPatternExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.regex = '\\d+'; + * }]); + * </script> + * <div ng-controller="ExampleController"> + * <form name="form"> + * <label for="regex">Set a pattern (regex string): </label> + * <input type="text" ng-model="regex" id="regex" /> + * <br> + * <label for="input">This input is restricted by the current pattern: </label> + * <input type="text" ng-model="model" id="input" name="input" ng-pattern="regex" /><br> + * <hr> + * input valid? = <code>{{form.input.$valid}}</code><br> + * model = <code>{{model}}</code> + * </form> + * </div> + * </file> + * <file name="protractor.js" type="protractor"> + var model = element(by.binding('model')); + var input = element(by.id('input')); + + it('should validate the input with the default pattern', function() { + input.sendKeys('aaa'); + expect(model.getText()).not.toContain('aaa'); + input.clear().then(function() { + input.sendKeys('123'); + expect(model.getText()).toContain('123'); + }); + }); + * </file> + * </example> + */ var patternDirective = function() { return { restrict: 'A', @@ -26364,14 +29476,80 @@ var patternDirective = function() { ctrl.$validate(); }); - ctrl.$validators.pattern = function(value) { - return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); + ctrl.$validators.pattern = function(modelValue, viewValue) { + // HTML5 pattern constraint validates the input value, so we validate the viewValue + return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue); }; } }; }; +/** + * @ngdoc directive + * @name ngMaxlength + * + * @description + * + * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * is longer than the integer obtained by evaluating the Angular expression given in the + * `ngMaxlength` attribute value. + * + * <div class="alert alert-info"> + * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two + * differences: + * <ol> + * <li> + * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint + * validation is not available. + * </li> + * <li> + * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be + * interpolated. + * </li> + * </ol> + * </div> + * + * @example + * <example name="ngMaxlengthDirective" module="ngMaxlengthExample"> + * <file name="index.html"> + * <script> + * angular.module('ngMaxlengthExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.maxlength = 5; + * }]); + * </script> + * <div ng-controller="ExampleController"> + * <form name="form"> + * <label for="maxlength">Set a maxlength: </label> + * <input type="number" ng-model="maxlength" id="maxlength" /> + * <br> + * <label for="input">This input is restricted by the current maxlength: </label> + * <input type="text" ng-model="model" id="input" name="input" ng-maxlength="maxlength" /><br> + * <hr> + * input valid? = <code>{{form.input.$valid}}</code><br> + * model = <code>{{model}}</code> + * </form> + * </div> + * </file> + * <file name="protractor.js" type="protractor"> + var model = element(by.binding('model')); + var input = element(by.id('input')); + + it('should validate the input with the default maxlength', function() { + input.sendKeys('abcdef'); + expect(model.getText()).not.toContain('abcdef'); + input.clear().then(function() { + input.sendKeys('abcde'); + expect(model.getText()).toContain('abcde'); + }); + }); + * </file> + * </example> + */ var maxlengthDirective = function() { return { restrict: 'A', @@ -26381,7 +29559,7 @@ var maxlengthDirective = function() { var maxlength = -1; attr.$observe('maxlength', function(value) { - var intVal = int(value); + var intVal = toInt(value); maxlength = isNaN(intVal) ? -1 : intVal; ctrl.$validate(); }); @@ -26392,6 +29570,70 @@ var maxlengthDirective = function() { }; }; +/** + * @ngdoc directive + * @name ngMinlength + * + * @description + * + * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * is shorter than the integer obtained by evaluating the Angular expression given in the + * `ngMinlength` attribute value. + * + * <div class="alert alert-info"> + * **Note:** This directive is also added when the plain `minlength` attribute is used, with two + * differences: + * <ol> + * <li> + * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint + * validation is not available. + * </li> + * <li> + * The `ngMinlength` value must be an expression, while the `minlength` value must be + * interpolated. + * </li> + * </ol> + * </div> + * + * @example + * <example name="ngMinlengthDirective" module="ngMinlengthExample"> + * <file name="index.html"> + * <script> + * angular.module('ngMinlengthExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.minlength = 3; + * }]); + * </script> + * <div ng-controller="ExampleController"> + * <form name="form"> + * <label for="minlength">Set a minlength: </label> + * <input type="number" ng-model="minlength" id="minlength" /> + * <br> + * <label for="input">This input is restricted by the current minlength: </label> + * <input type="text" ng-model="model" id="input" name="input" ng-minlength="minlength" /><br> + * <hr> + * input valid? = <code>{{form.input.$valid}}</code><br> + * model = <code>{{model}}</code> + * </form> + * </div> + * </file> + * <file name="protractor.js" type="protractor"> + var model = element(by.binding('model')); + var input = element(by.id('input')); + + it('should validate the input with the default minlength', function() { + input.sendKeys('ab'); + expect(model.getText()).not.toContain('ab'); + + input.sendKeys('abc'); + expect(model.getText()).toContain('abc'); + }); + * </file> + * </example> + */ var minlengthDirective = function() { return { restrict: 'A', @@ -26401,7 +29643,7 @@ var minlengthDirective = function() { var minlength = 0; attr.$observe('minlength', function(value) { - minlength = int(value) || 0; + minlength = toInt(value) || 0; ctrl.$validate(); }); ctrl.$validators.minlength = function(modelValue, viewValue) { @@ -26411,17 +29653,162 @@ var minlengthDirective = function() { }; }; - if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... +if (window.angular.bootstrap) { + //AngularJS is already loaded, so we can return here... + if (window.console) { console.log('WARNING: Tried to load angular more than once.'); - return; } + return; +} - //try to bind to jquery now so that one can write jqLite(document).ready() - //but we will rebind on bootstrap again. - bindJQuery(); +//try to bind to jquery now so that one can write jqLite(document).ready() +//but we will rebind on bootstrap again. +bindJQuery(); - publishExternalAPI(angular); +publishExternalAPI(angular); + +angular.module("ngLocale", [], ["$provide", function($provide) { +var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"}; +function getDecimals(n) { + n = n + ''; + var i = n.indexOf('.'); + return (i == -1) ? 0 : n.length - i - 1; +} + +function getVF(n, opt_precision) { + var v = opt_precision; + + if (undefined === v) { + v = Math.min(getDecimals(n), 3); + } + + var base = Math.pow(10, v); + var f = ((n * base) | 0) % base; + return {v: v, f: f}; +} + +$provide.value("$locale", { + "DATETIME_FORMATS": { + "AMPMS": [ + "AM", + "PM" + ], + "DAY": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "ERANAMES": [ + "Before Christ", + "Anno Domini" + ], + "ERAS": [ + "BC", + "AD" + ], + "FIRSTDAYOFWEEK": 6, + "MONTH": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "SHORTDAY": [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ], + "SHORTMONTH": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "STANDALONEMONTH": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], + "WEEKENDRANGE": [ + 5, + 6 + ], + "fullDate": "EEEE, MMMM d, y", + "longDate": "MMMM d, y", + "medium": "MMM d, y h:mm:ss a", + "mediumDate": "MMM d, y", + "mediumTime": "h:mm:ss a", + "short": "M/d/yy h:mm a", + "shortDate": "M/d/yy", + "shortTime": "h:mm a" + }, + "NUMBER_FORMATS": { + "CURRENCY_SYM": "$", + "DECIMAL_SEP": ".", + "GROUP_SEP": ",", + "PATTERNS": [ + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 3, + "minFrac": 0, + "minInt": 1, + "negPre": "-", + "negSuf": "", + "posPre": "", + "posSuf": "" + }, + { + "gSize": 3, + "lgSize": 3, + "maxFrac": 2, + "minFrac": 2, + "minInt": 1, + "negPre": "-\u00a4", + "negSuf": "", + "posPre": "\u00a4", + "posSuf": "" + } + ] + }, + "id": "en-us", + "localeID": "en_US", + "pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} +}); +}]); jqLite(document).ready(function() { angularInit(document, bootstrap); @@ -26429,4 +29816,4 @@ var minlengthDirective = function() { })(window, document); -!window.angular.$$csp() && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}</style>');
\ No newline at end of file +!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
\ No newline at end of file diff --git a/xstatic/pkg/angular/data/errors.json b/xstatic/pkg/angular/data/errors.json index 16dc001..a241884 100644 --- a/xstatic/pkg/angular/data/errors.json +++ b/xstatic/pkg/angular/data/errors.json @@ -1 +1 @@ -{"id":"ng","generated":"Tue Aug 18 2015 15:31:50 GMT-0700 (PDT)","errors":{"$http":{"badreq":"Http request configuration must be an object. Received: {0}"},"ngRepeat":{"badident":"alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}","iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'."},"$sce":{"imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","iwcard":"Illegal sequence *** in string matcher. String: {0}","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks mode. You can fix this by adding the text <!doctype html> to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","unsafe":"Attempting to use an unsafe value in a safe context.","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$controller":{"ctrlfmt":"Badly formed controller string '{0}'. Must match `__name__ as __id__` or `__name__`.","noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$parse":{"isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","ueoe":"Unexpected end of expression: {0}","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","isecobj":"Referencing Object in Angular expressions is disallowed! Expression: {0}","isecff":"Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecfld":"Attempting to access a disallowed field in Angular expressions! Expression: {0}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'."},"$q":{"norslvr":"Expected resolverFn, got '{0}'","qcycle":"Expected promise to be resolved with value other than itself '{0}'"},"$injector":{"pget":"Provider '{0}' must define $get factory method.","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","strictdi":"{0} is not using explicit annotation and cannot be invoked in strict mode","modulerr":"Failed to instantiate module {0} due to:\n{1}","undef":"Provider '{0}' must return a value from $get factory method.","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}"},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"ngModel":{"nonassign":"Expression '{0}' is non-assignable. Element: {1}","datefmt":"Expected `{0}` to be a date","$asyncValidators":"Expected asynchronous validator to return a promise but got '{0}' instead.","constexpr":"Expected constant expression for `{0}`, but saw `{1}`.","numfmt":"Expected `{0}` to be a number"},"$location":{"nostate":"History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API","ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","nobase":"$location in HTML5 mode requires a <base> tag to be present!"},"ng":{"areq":"Argument '{0}' is {1}","test":"no injector found for element argument to getTestability","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","btstrpd":"App Already Bootstrapped with this Element '{0}'","cpi":"Can't copy! Source and destination are identical.","badname":"hasOwnProperty is not a valid {0} name"},"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"$compile":{"selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","nonassign":"Expression '{0}' used with directive '{1}' is non-assignable!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","iscp":"Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}","baddir":"Directive name '{0}' is invalid. The first character must be a lowercase letter","multidir":"Multiple directives [{0}, {1}] asking for {2} on: {3}","tpload":"Failed to load template: {0}","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found."},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badmember":"Dotted member path \"@{0}\" is invalid.","badname":"hasOwnProperty is not a valid parameter name.","badcfg":"Error in resource configuration for action `{0}`. Expected response to contain an {1} but got an {2}"},"$route":{"norout":"Tried updating route when with no current route"},"$sanitize":{"badparse":"The sanitizer was unable to parse the following block of html: {0}"}}}
\ No newline at end of file +{"id":"ng","generated":"Wed Mar 16 2016 12:49:43 GMT-0700 (PDT)","errors":{"ng":{"areq":"Argument '{0}' is {1}","cpta":"Can't copy! TypedArray destination cannot be mutated.","test":"no injector found for element argument to getTestability","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","btstrpd":"App Already Bootstrapped with this Element '{0}'","cpi":"Can't copy! Source and destination are identical.","badname":"hasOwnProperty is not a valid {0} name"},"$http":{"legacy":"The method `{0}` on the promise returned from `$http` has been disabled.","badreq":"Http request configuration url must be a string. Received: {0}"},"ngRepeat":{"badident":"alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}","iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'."},"$sce":{"imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","iwcard":"Illegal sequence *** in string matcher. String: {0}","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks mode. You can fix this by adding the text <!doctype html> to the top of your HTML document. See http://docs.angularjs.org/api/ng.$sce for more information.","unsafe":"Attempting to use an unsafe value in a safe context.","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$controller":{"ctrlfmt":"Badly formed controller string '{0}'. Must match `__name__ as __id__` or `__name__`.","noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$parse":{"isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}","isecobj":"Referencing Object in Angular expressions is disallowed! Expression: {0}","lval":"Trying to assign a value to a non l-value","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","ueoe":"Unexpected end of expression: {0}","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","esc":"IMPOSSIBLE","iseccst":"Cannot convert object to primitive value! Expression: {0}","isecff":"Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecaf":"Assigning to a constructor is disallowed! Expression: {0}","isecfld":"Attempting to access a disallowed field in Angular expressions! Expression: {0}"},"jqLite":{"offargs":"jqLite#off() does not support the `selector` argument","onargs":"jqLite#on() does not support the `selector` or `eventData` parameters","nosel":"Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element"},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'.","nongcls":"$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the \"{0}\" CSS class."},"$q":{"norslvr":"Expected resolverFn, got '{0}'","qcycle":"Expected promise to be resolved with value other than itself '{0}'"},"$injector":{"pget":"Provider '{0}' must define $get factory method.","cdep":"Circular dependency found: {0}","nomod":"Module '{0}' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.","strictdi":"{0} is not using explicit annotation and cannot be invoked in strict mode","modulerr":"Failed to instantiate module {0} due to:\n{1}","undef":"Provider '{0}' must return a value from $get factory method.","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}"},"filter":{"notarray":"Expected array but received: {0}"},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"ngModel":{"nopromise":"Expected asynchronous validator to return a promise but got '{0}' instead.","nonassign":"Expression '{0}' is non-assignable. Element: {1}","datefmt":"Expected `{0}` to be a date","constexpr":"Expected constant expression for `{0}`, but saw `{1}`.","numfmt":"Expected `{0}` to be a number"},"$location":{"nostate":"History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API","ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","nobase":"$location in HTML5 mode requires a <base> tag to be present!"},"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"$interpolate":{"noconcat":"Error while interpolating: {0}\nStrict Contextual Escaping disallows interpolations that concatenate multiple expressions when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce","interr":"Can't interpolate: {0}\n{1}","nochgmustache":"angular-message-format.js currently does not allow you to use custom start and end symbols for interpolation.","reqcomma":"Expected a comma after the keyword “{0}” at line {1}, column {2} of text “{3}”","untermstr":"The string beginning at line {0}, column {1} is unterminated in text “{2}”","badexpr":"Unexpected operator “{0}” at line {1}, column {2} in text. Was expecting “{3}”. Text: “{4}”","dupvalue":"The choice “{0}” is specified more than once. Duplicate key is at line {1}, column {2} in text “{3}”","unsafe":"Use of select/plural MessageFormat syntax is currently disallowed in a secure context ({0}). At line {1}, column {2} of text “{3}”","reqother":"“other” is a required option.","reqendinterp":"Expecting end of interpolation symbol, “{0}”, at line {1}, column {2} in text “{3}”","reqarg":"Expected one of “plural” or “select” at line {0}, column {1} of text “{2}”","wantstring":"Expected the beginning of a string at line {0}, column {1} in text “{2}”","logicbug":"The messageformat parser has encountered an internal error. Please file a github issue against the AngularJS project and provide this message text that triggers the bug. Text: “{0}”","reqopenbrace":"The plural choice “{0}” must be followed by a message in braces at line {1}, column {2} in text “{3}”","unknarg":"Unsupported keyword “{0}” at line {0}, column {1}. Only “plural” and “select” are currently supported. Text: “{3}”","reqendbrace":"The plural/select choice “{0}” message starting at line {1}, column {2} does not have an ending closing brace. Text “{3}”"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"$compile":{"noident":"Cannot bind to controller without identifier for directive '{0}'.","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","nonassign":"Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","iscp":"Invalid {3} for directive '{0}'. Definition: {... {1}: '{2}' ...}","baddir":"Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces","noctrl":"Cannot bind to controller without directive '{0}'s controller.","multidir":"Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}","tpload":"Failed to load template: {0} (HTTP status: {1} {2})","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found."},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badmember":"Dotted member path \"@{0}\" is invalid.","badname":"hasOwnProperty is not a valid parameter name.","badcfg":"Error in resource configuration for action `{0}`. Expected response to contain an {1} but got an {2} (Request: {3} {4})"},"$route":{"norout":"Tried updating route when with no current route"},"$sanitize":{"badparse":"The sanitizer was unable to parse the following block of html: {0}"}}}
\ No newline at end of file diff --git a/xstatic/pkg/angular/data/version.json b/xstatic/pkg/angular/data/version.json index 3294b3f..a94808b 100644 --- a/xstatic/pkg/angular/data/version.json +++ b/xstatic/pkg/angular/data/version.json @@ -1 +1 @@ -{"raw":"v1.3.18","major":1,"minor":3,"patch":18,"prerelease":[],"build":[],"version":"1.3.18","codeName":"collective-penmanship","full":"1.3.18","branch":"v1.3.x","cdn":{"raw":"v1.3.17","major":1,"minor":3,"patch":17,"prerelease":[],"build":[],"version":"1.3.17","docsUrl":"http://code.angularjs.org/1.3.17/docs"}}
\ No newline at end of file +{"raw":"v1.4.10","major":1,"minor":4,"patch":10,"prerelease":[],"build":[],"version":"1.4.10","codeName":"benignant-oscillation","full":"1.4.10","branch":"v1.4.x","cdn":{"raw":"v1.4.9","major":1,"minor":4,"patch":9,"prerelease":[],"build":[],"version":"1.4.9","docsUrl":"http://code.angularjs.org/1.4.9/docs"}}
\ No newline at end of file diff --git a/xstatic/pkg/angular/data/version.txt b/xstatic/pkg/angular/data/version.txt index 672b322..3659506 100644 --- a/xstatic/pkg/angular/data/version.txt +++ b/xstatic/pkg/angular/data/version.txt @@ -1 +1 @@ -1.3.18
\ No newline at end of file +1.4.10
\ No newline at end of file |