diff options
author | Travis Tripp <travis.tripp@hp.com> | 2014-12-17 10:49:18 -0700 |
---|---|---|
committer | Travis Tripp <travis.tripp@hp.com> | 2014-12-18 11:04:11 -0700 |
commit | dac047df05da5bb20de4e78876bc03820d0c6671 (patch) | |
tree | 755e12a695b93e8e23dd035bee7e351256529457 /xstatic/pkg | |
parent | 7300bd88ba20368ef86c93428f78acac6f5d9c71 (diff) | |
download | xstatic-angular-dac047df05da5bb20de4e78876bc03820d0c6671.tar.gz |
Angular xstatic for Version 1.3.71.3.7.0
Adding version 1.3.7
1.3.x angular is needed for launch instance work.
Partially Implements: blueprint launch-instance-redesign
Change-Id: Ib45130a624cace9139a5309470f70ed2983b8cb3
Diffstat (limited to 'xstatic/pkg')
-rw-r--r-- | xstatic/pkg/angular/__init__.py | 2 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-animate.js | 1521 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-aria.js | 332 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-cookies.js | 42 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-csp.css | 7 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-loader.js | 71 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-messages.js | 400 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-mocks.js | 529 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-resource.js | 555 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-route.js | 226 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-sanitize.js | 260 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-scenario.js | 23057 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular-touch.js | 91 | ||||
-rw-r--r-- | xstatic/pkg/angular/data/angular.js | 14207 | ||||
-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 |
17 files changed, 25752 insertions, 15554 deletions
diff --git a/xstatic/pkg/angular/__init__.py b/xstatic/pkg/angular/__init__.py index f3983c4..f70663b 100644 --- a/xstatic/pkg/angular/__init__.py +++ b/xstatic/pkg/angular/__init__.py @@ -11,7 +11,7 @@ NAME = __name__.split('.')[-1] # package name (e.g. 'foo' or 'foo_bar') # please use a all-lowercase valid python # package name -VERSION = '1.2.16' # version of the packaged files, please use the upstream +VERSION = '1.3.7' # version of the packaged files, please use the upstream # version number BUILD = '0' # our package build number, so we can release new builds # with fixes for xstatic stuff. diff --git a/xstatic/pkg/angular/data/angular-animate.js b/xstatic/pkg/angular/data/angular-animate.js index 9a0af80..7d09d78 100644 --- a/xstatic/pkg/angular/data/angular-animate.js +++ b/xstatic/pkg/angular/data/angular-animate.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -12,33 +12,31 @@ * @name ngAnimate * @description * - * # ngAnimate - * * 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: + * 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#usage_animations ngRepeat} | enter, leave and move | - * | {@link ngRoute.directive:ngView#usage_animations ngView} | enter and leave | - * | {@link ng.directive:ngInclude#usage_animations ngInclude} | enter and leave | - * | {@link ng.directive:ngSwitch#usage_animations ngSwitch} | enter and leave | - * | {@link ng.directive:ngIf#usage_animations ngIf} | enter and leave | - * | {@link ng.directive:ngClass#usage_animations ngClass} | add and remove | - * | {@link ng.directive:ngShow#usage_animations ngShow & ngHide} | add and remove (the ng-hide class value) | - * | {@link ng.directive:form#usage_animations form} | add and remove (dirty, pristine, valid, invalid & all other validations) | - * | {@link ng.directive:ngModel#usage_animations ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | Directive | Supported Animations | + * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link module:ngMessages#animations ngMessage} | enter and leave | * * You can find out more information about animations upon visiting each directive page. * @@ -52,9 +50,9 @@ * } * * .slide.ng-enter { } /* starting animations for enter */ - * .slide.ng-enter-active { } /* terminal animations for enter */ + * .slide.ng-enter.ng-enter-active { } /* terminal animations for enter */ * .slide.ng-leave { } /* starting animations for leave */ - * .slide.ng-leave-active { } /* terminal animations for leave */ + * .slide.ng-leave.ng-leave-active { } /* terminal animations for leave */ * </style> * * <!-- @@ -64,10 +62,34 @@ * <ANY class="slide" ng-include="..."></ANY> * ``` * - * Keep in mind that if an animation is running, any child elements cannot be animated until the parent element's - * animation has completed. + * Keep in mind that, by default, if an animation is running, any child elements cannot be animated + * until the parent element's animation has completed. This blocking feature can be overridden by + * placing the `ng-animate-children` attribute on a parent container tag. + * + * ```html + * <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> + * </div> + * ``` + * + * When the `on` expression value changes and an animation is triggered then each of the elements within + * will all animate without the block being applied to child elements. + * + * ## Are animations run when the application starts? + * No they are not. When an application is bootstrapped Angular will disable animations from running to avoid + * a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work, + * Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering + * layout changes in the application will trigger animations as normal. * - * <h2>CSS-defined Animations</h2> + * 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. @@ -136,9 +158,100 @@ * 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. * - * <h3>CSS Staggering Animations</h3> + * ### Structural transition animations + * + * Structural transitions (such as enter, leave and move) will always apply a `0s none` transition + * value to force the browser into rendering the styles defined in the setup (`.ng-enter`, `.ng-leave` + * or `.ng-move`) class. This means that any active transition animations operating on the element + * will be cut off to make way for the enter, leave or move animation. + * + * ### Class-based transition animations + * + * Class-based transitions refer to transition animations that are triggered when a CSS class is + * added to or removed from the element (via `$animate.addClass`, `$animate.removeClass`, + * `$animate.setClass`, or by directives such as `ngClass`, `ngModel` and `form`). + * They are different when compared to structural animations since they **do not cancel existing + * animations** nor do they **block successive transitions** from rendering on the same element. + * This distinction allows for **multiple class-based transitions** to be performed on the same element. + * + * In addition to ngAnimate supporting the default (natural) functionality of class-based transition + * animations, ngAnimate also decorates the element with starting and ending CSS classes to aid the + * developer in further styling the element throughout the transition animation. Earlier versions + * of ngAnimate may have caused natural CSS transitions to break and not render properly due to + * $animate temporarily blocking transitions using `0s none` in order to allow the setup CSS class + * (the `-add` or `-remove` class) to be applied without triggering an animation. However, as of + * **version 1.3**, this workaround has been removed with ngAnimate and all non-ngAnimate CSS + * class transitions are compatible with ngAnimate. + * + * There is, however, one special case when dealing with class-based transitions in ngAnimate. + * When rendering class-based transitions that make use of the setup and active CSS classes + * (e.g. `.fade-add` and `.fade-add-active` for when `.fade` is added) be sure to define + * the transition value **on the active CSS class** and not the setup class. + * + * ```css + * .fade-add { + * /* remember to place a 0s transition here + * to ensure that the styles are applied instantly + * even if the element already has a transition style */ + * transition:0s linear all; + * + * /* starting CSS styles */ + * opacity:1; + * } + * .fade-add.fade-add-active { + * /* this will be the length of the animation */ + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * The setup CSS class (in this case `.fade-add`) also has a transition style property, however, it + * has a duration of zero. This may not be required, however, incase the browser is unable to render + * the styling present in this CSS class instantly then it could be that the browser is attempting + * to perform an unnecessary transition. + * + * This workaround, however, does not apply to standard class-based transitions that are rendered + * when a CSS class containing a transition is applied to an element: + * + * ```css + * /* this works as expected */ + * .fade { + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * Please keep this in mind when coding the CSS markup that will be used within class-based transitions. + * Also, try not to mix the two class-based animation flavors together since the CSS code may become + * overly complex. + * + * + * ### Preventing Collisions With Third Party Libraries + * + * Some third-party frameworks place animation duration defaults across many element or className + * selectors in order to make their code small and reuseable. This can lead to issues with ngAnimate, which + * is expecting actual animations on these elements and has to wait for their completion. + * + * You can prevent this unwanted behavior by using a prefix on all your animation classes: + * + * ```css + * /* prefixed with animate- */ + * .animate-fade-add.animate-fade-add-active { + * transition:1s linear all; + * opacity:0; + * } + * ``` + * + * You then configure `$animate` to enforce this prefix: + * + * ```js + * $animateProvider.classNameFilter(/animate-/); + * ``` + * </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, as of 1.2.0, supports staggering animations and the stagger effect can be + * 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). @@ -191,7 +304,7 @@ * * Stagger animations are currently only supported within CSS-defined animations. * - * <h2>JavaScript-defined Animations</h2> + * ## 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. * @@ -237,6 +350,49 @@ * and the JavaScript animation is found, then the enter callback will handle that animation (in addition to the CSS keyframe animation * or transition code that is defined via a stylesheet). * + * + * ### Applying Directive-specific Styles to an Animation + * In some cases a directive or service may want to provide `$animate` with extra details that the animation will + * include into its animation. Let's say for example we wanted to render an animation that animates an element + * towards the mouse coordinates as to where the user clicked last. By collecting the X/Y coordinates of the click + * (via the event parameter) we can set the `top` and `left` styles into an object and pass that into our function + * call to `$animate.addClass`. + * + * ```js + * canvas.on('click', function(e) { + * $animate.addClass(element, 'on', { + * to: { + * left : e.client.x + 'px', + * top : e.client.y + 'px' + * } + * }): + * }); + * ``` + * + * Now when the animation runs, and a transition or keyframe animation is picked up, then the animation itself will + * also include and transition the styling of the `left` and `top` properties into its running animation. If we want + * to provide some starting animation values then we can do so by placing the starting animations styles into an object + * called `from` in the same object as the `to` animations. + * + * ```js + * canvas.on('click', function(e) { + * $animate.addClass(element, 'on', { + * from: { + * position: 'absolute', + * left: '0px', + * top: '0px' + * }, + * to: { + * left : e.client.x + 'px', + * top : e.client.y + 'px' + * } + * }): + * }); + * ``` + * + * Once the animation is complete or cancelled then the union of both the before and after styles are applied to the + * element. If `ngAnimate` is not present then the styles will be applied immediately. + * */ angular.module('ngAnimate', ['ng']) @@ -255,6 +411,19 @@ angular.module('ngAnimate', ['ng']) * Please visit the {@link ngAnimate `ngAnimate`} module overview page learn more about how to use animations in your application. * */ + .directive('ngAnimateChildren', function() { + var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; + return function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN, true); + } else { + scope.$watch(val, function(value) { + element.data(NG_ANIMATE_CHILDREN, !!value); + }); + } + }; + }) //this private service is only used within CSS-enabled animations //IE8 + IE9 do not support rAF natively, but that is fine since they @@ -280,21 +449,29 @@ angular.module('ngAnimate', ['ng']) var noop = angular.noop; var forEach = angular.forEach; var selectors = $animateProvider.$$selectors; + var isArray = angular.isArray; + var isString = angular.isString; + var isObject = angular.isObject; var ELEMENT_NODE = 1; var NG_ANIMATE_STATE = '$$ngAnimateState'; + var NG_ANIMATE_CHILDREN = '$$ngAnimateChildren'; var NG_ANIMATE_CLASS_NAME = 'ng-animate'; var rootAnimateState = {running: true}; function extractElementNode(element) { - for(var i = 0; i < element.length; i++) { + for (var i = 0; i < element.length; i++) { var elm = element[i]; - if(elm.nodeType == ELEMENT_NODE) { + if (elm.nodeType == ELEMENT_NODE) { return elm; } } } + function prepareElement(element) { + return element && angular.element(element); + } + function stripCommentsFromElement(element) { return angular.element(extractElementNode(element)); } @@ -302,25 +479,40 @@ angular.module('ngAnimate', ['ng']) function isMatchingElement(elm1, elm2) { return extractElementNode(elm1) == extractElementNode(elm2); } + var $$jqLite; + $provide.decorator('$animate', + ['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest', '$$jqLite', + function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest, $$$jqLite) { - $provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', - function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) { - - var globalAnimationCounter = 0; + $$jqLite = $$$jqLite; $rootElement.data(NG_ANIMATE_STATE, rootAnimateState); - // disable animations during bootstrap, but once we bootstrapped, wait again - // for another digest until enabling animations. The reason why we digest twice - // is because all structural animations (enter, leave and move) all perform a - // post digest operation before animating. If we only wait for a single digest - // to pass then the structural animation would render its animation on page load. - // (which is what we're trying to avoid when the application first boots up.) - $rootScope.$$postDigest(function() { - $rootScope.$$postDigest(function() { - rootAnimateState.running = false; - }); - }); + // 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; } @@ -328,6 +520,85 @@ angular.module('ngAnimate', ['ng']) return classNameFilter.test(className); }; + function classBasedAnimationsBlocked(element, setter) { + var data = element.data(NG_ANIMATE_STATE) || {}; + if (setter) { + data.running = true; + data.structural = true; + element.data(NG_ANIMATE_STATE, data); + } + return data.disabled || (data.running && data.structural); + } + + function runAnimationPostDigest(fn) { + var cancelFn, defer = $$q.defer(); + defer.promise.$$cancelFn = function() { + cancelFn && cancelFn(); + }; + $rootScope.$$postDigest(function() { + cancelFn = fn(function() { + defer.resolve(); + }); + }); + return defer.promise; + } + + function parseAnimateOptions(options) { + // some plugin code may still be passing in the callback + // function as the last param for the $animate methods so + // it's best to only allow string or array values for now + if (isObject(options)) { + if (options.tempClasses && isString(options.tempClasses)) { + options.tempClasses = options.tempClasses.split(/\s+/); + } + return options; + } + } + + function resolveElementClasses(element, cache, runningAnimations) { + runningAnimations = runningAnimations || {}; + + var lookup = {}; + forEach(runningAnimations, function(data, selector) { + forEach(selector.split(' '), function(s) { + lookup[s]=data; + }); + }); + + var hasClasses = Object.create(null); + forEach((element.attr('class') || '').split(/\s+/), function(className) { + hasClasses[className] = true; + }); + + var toAdd = [], toRemove = []; + forEach((cache && cache.classes) || [], function(status, className) { + var hasClass = hasClasses[className]; + var matchingAnimation = lookup[className] || {}; + + // When addClass and removeClass is called then $animate will check to + // see if addClass and removeClass cancel each other out. When there are + // more calls to removeClass than addClass then the count falls below 0 + // and then the removeClass animation will be allowed. Otherwise if the + // count is above 0 then that means an addClass animation will commence. + // Once an animation is allowed then the code will also check to see if + // there exists any on-going animation that is already adding or remvoing + // the matching CSS class. + if (status === false) { + //does it have the class or will it have the class + if (hasClass || matchingAnimation.event == 'addClass') { + toRemove.push(className); + } + } else if (status === true) { + //is the class missing or will it be removed? + if (!hasClass || matchingAnimation.event == 'removeClass') { + toAdd.push(className); + } + } + }); + + return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')]; + } + function lookup(name) { if (name) { var matches = [], @@ -346,10 +617,10 @@ angular.module('ngAnimate', ['ng']) matches.push($injector.get(selectors[''])); } - for(var i=0; i < classes.length; i++) { + for (var i=0; i < classes.length; i++) { var klass = classes[i], selectorFactoryName = selectors[klass]; - if(selectorFactoryName && !flagMap[klass]) { + if (selectorFactoryName && !flagMap[klass]) { matches.push($injector.get(selectorFactoryName)); flagMap[klass] = true; } @@ -358,29 +629,44 @@ angular.module('ngAnimate', ['ng']) } } - function animationRunner(element, animationEvent, className) { + 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) { + if (!node) { return; } - var isSetClassOperation = animationEvent == 'setClass'; - var isClassBased = isSetClassOperation || - animationEvent == 'addClass' || - animationEvent == 'removeClass'; + if (options) { + options.to = options.to || {}; + options.from = options.from || {}; + } - var classNameAdd, classNameRemove; - if(angular.isArray(className)) { + var classNameAdd; + var classNameRemove; + if (isArray(className)) { classNameAdd = className[0]; classNameRemove = className[1]; - className = classNameAdd + ' ' + classNameRemove; + if (!classNameAdd) { + className = classNameRemove; + animationEvent = 'removeClass'; + } else if (!classNameRemove) { + className = classNameAdd; + animationEvent = 'addClass'; + } else { + className = classNameAdd + ' ' + classNameRemove; + } } + var isSetClassOperation = animationEvent == 'setClass'; + var isClassBased = isSetClassOperation + || animationEvent == 'addClass' + || animationEvent == 'removeClass' + || animationEvent == 'animate'; + var currentClassName = element.attr('class'); var classes = currentClassName + ' ' + className; - if(!isAnimatableClassName(classes)) { + if (!isAnimatableClassName(classes)) { return; } @@ -394,7 +680,7 @@ angular.module('ngAnimate', ['ng']) var animationLookup = (' ' + classes).replace(/\s+/g,'.'); forEach(lookup(animationLookup), function(animationFactory) { var created = registerAnimation(animationFactory, animationEvent); - if(!created && isSetClassOperation) { + if (!created && isSetClassOperation) { registerAnimation(animationFactory, 'addClass'); registerAnimation(animationFactory, 'removeClass'); } @@ -403,17 +689,17 @@ angular.module('ngAnimate', ['ng']) 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') { + if (afterFn || beforeFn) { + if (event == 'leave') { beforeFn = afterFn; //when set as null then animation knows to skip this phase afterFn = null; } after.push({ - event : event, fn : afterFn + event: event, fn: afterFn }); before.push({ - event : event, fn : beforeFn + event: event, fn: beforeFn }); return true; } @@ -427,9 +713,9 @@ angular.module('ngAnimate', ['ng']) var count = 0; function afterAnimationComplete(index) { - if(cancellations) { + if (cancellations) { (cancellations[index] || noop)(); - if(++count < animations.length) return; + if (++count < animations.length) return; cancellations = null; } allCompleteFn(); @@ -442,55 +728,63 @@ angular.module('ngAnimate', ['ng']) var progress = function() { afterAnimationComplete(index); }; - switch(animation.event) { + switch (animation.event) { case 'setClass': - cancellations.push(animation.fn(element, classNameAdd, classNameRemove, progress)); + 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)); + cancellations.push(animation.fn(element, classNameAdd || className, progress, options)); break; case 'removeClass': - cancellations.push(animation.fn(element, classNameRemove || className, progress)); + cancellations.push(animation.fn(element, classNameRemove || className, progress, options)); break; default: - cancellations.push(animation.fn(element, progress)); + cancellations.push(animation.fn(element, progress, options)); break; } }); - if(cancellations && cancellations.length === 0) { + if (cancellations && cancellations.length === 0) { allCompleteFn(); } } return { - node : node, - event : animationEvent, - className : className, - isClassBased : isClassBased, - isSetClassOperation : isSetClassOperation, - before : function(allCompleteFn) { + node: node, + event: animationEvent, + className: className, + isClassBased: isClassBased, + isSetClassOperation: isSetClassOperation, + applyStyles: function() { + if (options) { + element.css(angular.extend(options.from || {}, options.to || {})); + } + }, + before: function(allCompleteFn) { beforeComplete = allCompleteFn; run(before, beforeCancel, function() { beforeComplete = noop; allCompleteFn(); }); }, - after : function(allCompleteFn) { + after: function(allCompleteFn) { afterComplete = allCompleteFn; run(after, afterCancel, function() { afterComplete = noop; allCompleteFn(); }); }, - cancel : function() { - if(beforeCancel) { + cancel: function() { + if (beforeCancel) { forEach(beforeCancel, function(cancelFn) { (cancelFn || noop)(true); }); beforeComplete(true); } - if(afterCancel) { + if (afterCancel) { forEach(afterCancel, function(cancelFn) { (cancelFn || noop)(true); }); @@ -503,7 +797,7 @@ angular.module('ngAnimate', ['ng']) /** * @ngdoc service * @name $animate - * @function + * @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. @@ -517,13 +811,112 @@ angular.module('ngAnimate', ['ng']) * 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').then(function() { + * //this will still be called even if cancelled + * }); + * + * element.on('click', function() { + * //tooo lazy to wait for the animation to end + * $animate.cancel(promise); + * }); + * ``` + * + * (Keep in mind that the promise cancellation is unique to `$animate` since promises in + * general cannot be cancelled.) * */ return { /** * @ngdoc method + * @name $animate#animate + * @kind function + * + * @description + * Performs an inline animation on the element which applies the provided `to` and `from` CSS styles to the element. + * If any detected CSS transition, keyframe or JavaScript matches the provided `className` value then the animation + * will take on the provided styles. For example, if a transition animation is set for the given className then the + * provided `from` and `to` styles will be applied alongside the given transition. If a JavaScript animation is + * detected then the provided styles will be given in as function paramters. + * + * ```js + * ngModule.animation('.my-inline-animation', function() { + * return { + * animate : function(element, className, from, to, done) { + * //styles + * } + * } + * }); + * ``` + * + * Below is a breakdown of each step that occurs during the `animate` animation: + * + * | Animation Step | What the element class attribute looks like | + * |-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------| + * | 1. `$animate.animate(...)` is called | `class="my-animation"` | + * | 2. `$animate` waits for the next digest to start the animation | `class="my-animation ng-animate"` | + * | 3. `$animate` runs the JavaScript-defined animations detected on the element | `class="my-animation ng-animate"` | + * | 4. the `className` class value is added to the element | `class="my-animation ng-animate className"` | + * | 5. `$animate` scans the element styles to get the CSS transition/animation duration and delay | `class="my-animation ng-animate className"` | + * | 6. `$animate` blocks all CSS transitions on the element to ensure the `.className` class styling is applied right away| `class="my-animation ng-animate className"` | + * | 7. `$animate` applies the provided collection of `from` CSS styles to the element | `class="my-animation ng-animate className"` | + * | 8. `$animate` waits for a single animation frame (this performs a reflow) | `class="my-animation ng-animate className"` | + * | 9. `$animate` removes the CSS transition block placed on the element | `class="my-animation ng-animate className"` | + * | 10. the `className-active` class is added (this triggers the CSS transition/animation) | `class="my-animation ng-animate className className-active"` | + * | 11. `$animate` applies the collection of `to` CSS styles to the element which are then handled by the transition | `class="my-animation ng-animate className className-active"` | + * | 12. `$animate` waits for the animation to complete (via events and timeout) | `class="my-animation ng-animate className className-active"` | + * | 13. The animation ends and all generated CSS classes are removed from the element | `class="my-animation"` | + * | 14. The returned promise is resolved. | `class="my-animation"` | + * + * @param {DOMElement} element the element that will be the focus of the enter animation + * @param {object} from a collection of CSS styles that will be applied to the element at the start of the animation + * @param {object} to a collection of CSS styles that the element will animate towards + * @param {string=} className an optional CSS class that will be added to the element for the duration of the animation (the default class is `ng-inline-animate`) + * @param {object=} options an optional collection of options that will be picked up by the CSS transition/animation + * @return {Promise} the animation callback promise + */ + animate: function(element, from, to, className, options) { + className = className || 'ng-inline-animate'; + options = parseAnimateOptions(options) || {}; + options.from = to ? from : null; + options.to = to ? to : from; + + return runAnimationPostDigest(function(done) { + return performAnimation('animate', className, stripCommentsFromElement(element), null, null, noop, options, done); + }); + }, + + /** + * @ngdoc method * @name $animate#enter - * @function + * @kind function * * @description * Appends the element to the parentElement element that resides in the document and then runs the enter animation. Once @@ -531,37 +924,45 @@ angular.module('ngAnimate', ['ng']) * * 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 runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 4. the .ng-enter class is added to the element | class="my-animation ng-animate ng-enter" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-enter" | - * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-enter" | - * | 7. the .ng-enter-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | - * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-enter ng-enter-active" | - * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-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 {function()=} doneCallback the callback function that will be called once the animation is complete + * @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, doneCallback) { - this.enabled(false, element); + 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); - $rootScope.$$postDigest(function() { - element = stripCommentsFromElement(element); - performAnimation('enter', 'ng-enter', element, parentElement, afterElement, noop, doneCallback); + return runAnimationPostDigest(function(done) { + return performAnimation('enter', 'ng-enter', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); }); }, /** * @ngdoc method * @name $animate#leave - * @function + * @kind function * * @description * Runs the leave animation operation and, upon completion, removes the element from the DOM. Once @@ -569,36 +970,43 @@ angular.module('ngAnimate', ['ng']) * * 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 any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 3. the .ng-leave class is added to the element | class="my-animation ng-animate ng-leave" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-leave" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-leave" | - * | 6. the .ng-leave-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-leave ng-leave-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The element is removed from the DOM | ... | - * | 10. The doneCallback() callback is fired (if provided) | ... | + * | 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 {function()=} doneCallback the callback function that will be called once the animation is complete + * @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, doneCallback) { + leave: function(element, options) { + options = parseAnimateOptions(options); + element = angular.element(element); + cancelChildAnimations(element); - this.enabled(false, element); - $rootScope.$$postDigest(function() { - performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { + classBasedAnimationsBlocked(element, true); + return runAnimationPostDigest(function(done) { + return performAnimation('leave', 'ng-leave', stripCommentsFromElement(element), null, null, function() { $delegate.leave(element); - }, doneCallback); + }, options, done); }); }, /** * @ngdoc method * @name $animate#move - * @function + * @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 @@ -607,31 +1015,39 @@ angular.module('ngAnimate', ['ng']) * * 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 runs any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 4. the .ng-move class is added to the element | class="my-animation ng-animate ng-move" | - * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate ng-move" | - * | 6. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate ng-move" | - * | 7. the .ng-move-active and .ng-animate-active classes is added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | - * | 8. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active ng-move ng-move-active" | - * | 9. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 10. The doneCallback() callback is fired (if provided) | class="my-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 {function()=} doneCallback the callback function that will be called once the animation is complete + * @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, doneCallback) { + move: function(element, parentElement, afterElement, options) { + options = parseAnimateOptions(options); + element = angular.element(element); + parentElement = prepareElement(parentElement); + afterElement = prepareElement(afterElement); + cancelChildAnimations(element); - this.enabled(false, element); + classBasedAnimationsBlocked(element, true); $delegate.move(element, parentElement, afterElement); - $rootScope.$$postDigest(function() { - element = stripCommentsFromElement(element); - performAnimation('move', 'ng-move', element, parentElement, afterElement, noop, doneCallback); + return runAnimationPostDigest(function(done) { + return performAnimation('move', 'ng-move', stripCommentsFromElement(element), parentElement, afterElement, noop, options, done); }); }, @@ -643,32 +1059,30 @@ angular.module('ngAnimate', ['ng']) * 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 or base CSS class). + * 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 any JavaScript-defined animations on the element | class="my-animation ng-animate" | - * | 3. the .super-add class are added to the element | class="my-animation ng-animate super-add" | - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-animate super-add" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation ng-animate super-add" | - * | 6. the .super, .super-add-active and .ng-animate-active classes are added (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super super-add super-add-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation 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 doneCallback() callback is fired (if provided) | class="my-animation super" | + * | 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 {function()=} doneCallback the callback function that will be called once the animation is complete + * @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, doneCallback) { - element = stripCommentsFromElement(element); - performAnimation('addClass', className, element, null, null, function() { - $delegate.addClass(element, className); - }, doneCallback); + addClass: function(element, className, options) { + return this.setClass(element, className, [], options); }, /** @@ -683,55 +1097,147 @@ angular.module('ngAnimate', ['ng']) * * 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 any JavaScript-defined animations on the element | class="my-animation super ng-animate" | - * | 3. the .super-remove class are added to the element | class="my-animation super ng-animate super-remove"| - * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation super ng-animate super-remove" | - * | 5. $animate waits for 10ms (this performs a reflow) | class="my-animation super ng-animate super-remove" | - * | 6. the .super-remove-active and .ng-animate-active classes are added and .super is removed (this triggers the CSS transition/animation) | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | - * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-animate ng-animate-active super-remove super-remove-active" | - * | 8. The animation ends and all generated CSS classes are removed from the element | class="my-animation" | - * | 9. The doneCallback() callback is fired (if provided) | class="my-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 {function()=} doneCallback the callback function that will be called once the animation is complete + * @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, doneCallback) { - element = stripCommentsFromElement(element); - performAnimation('removeClass', className, element, null, null, function() { - $delegate.removeClass(element, className); - }, doneCallback); + removeClass: function(element, className, options) { + return this.setClass(element, [], className, options); }, - /** - * - * @ngdoc function - * @name $animate#setClass - * @function - * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will it's 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 {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element - */ - setClass : function(element, add, remove, doneCallback) { + /** + * + * @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); - performAnimation('setClass', [add, remove], element, null, null, function() { - $delegate.setClass(element, add, remove); - }, doneCallback); + + if (classBasedAnimationsBlocked(element)) { + return $delegate.$$setClassImmediately(element, add, remove, options); + } + + // we're using a combined array for both the add and remove + // operations since the ORDER OF addClass and removeClass matters + var classes, cache = element.data(STORAGE_KEY); + var hasCache = !!cache; + if (!cache) { + cache = {}; + cache.classes = {}; + } + classes = cache.classes; + + add = isArray(add) ? add : add.split(' '); + forEach(add, function(c) { + if (c && c.length) { + classes[c] = true; + } + }); + + remove = isArray(remove) ? remove : remove.split(' '); + forEach(remove, function(c) { + if (c && c.length) { + classes[c] = false; + } + }); + + if (hasCache) { + if (options && cache.options) { + cache.options = angular.extend(cache.options || {}, options); + } + + //the digest cycle will combine all the animations into one function + return cache.promise; + } else { + element.data(STORAGE_KEY, cache = { + classes: classes, + options: options + }); + } + + return cache.promise = runAnimationPostDigest(function(done) { + var parentElement = element.parent(); + var elementNode = extractElementNode(element); + var parentNode = elementNode.parentNode; + // TODO(matsko): move this code into the animationsDisabled() function once #8092 is fixed + if (!parentNode || parentNode['$$NG_REMOVED'] || elementNode['$$NG_REMOVED']) { + done(); + return; + } + + var cache = element.data(STORAGE_KEY); + element.removeData(STORAGE_KEY); + + var state = element.data(NG_ANIMATE_STATE) || {}; + var classes = resolveElementClasses(element, cache, state.active); + return !classes + ? done() + : performAnimation('setClass', classes, element, parentElement, null, function() { + if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]); + if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]); + }, cache.options, done); + }); + }, + + /** + * @ngdoc method + * @name $animate#cancel + * @kind function + * + * @param {Promise} animationPromise The animation promise that is returned when an animation is started. + * + * @description + * Cancels the provided animation. + */ + cancel: function(promise) { + promise.$$cancelFn(); }, /** * @ngdoc method * @name $animate#enabled - * @function + * @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 @@ -741,10 +1247,10 @@ angular.module('ngAnimate', ['ng']) * Globally enables/disables animations. * */ - enabled : function(value, element) { - switch(arguments.length) { + enabled: function(value, element) { + switch (arguments.length) { case 2: - if(value) { + if (value) { cleanup(element); } else { var data = element.data(NG_ANIMATE_STATE) || {}; @@ -769,20 +1275,21 @@ angular.module('ngAnimate', ['ng']) 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 + 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, doneCallback) { - - var runner = animationRunner(element, animationEvent, className); - if(!runner) { + 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; + return noopCancel; } + animationEvent = runner.event; className = runner.className; var elementEvents = angular.element._data(runner.node); elementEvents = elementEvents && elementEvents.events; @@ -791,51 +1298,44 @@ angular.module('ngAnimate', ['ng']) parentElement = afterElement ? afterElement.parent() : element.parent(); } - var ngAnimateState = element.data(NG_ANIMATE_STATE) || {}; - var runningAnimations = ngAnimateState.active || {}; - var totalActiveAnimations = ngAnimateState.totalActive || 0; - var lastAnimation = ngAnimateState.last; - - //only allow animations if the currently running animation is not structural - //or if there is no animation running at all - var skipAnimations = runner.isClassBased ? - ngAnimateState.disabled || (lastAnimation && !lastAnimation.isClassBased) : - false; - //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 (skipAnimations || animationsDisabled(element, parentElement)) { + if (animationsDisabled(element, parentElement)) { fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); closeAnimation(); - return; + 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) { + + if (totalActiveAnimations > 0) { var animationsToCancel = []; - if(!runner.isClassBased) { - if(animationEvent == 'leave' && runningAnimations['ng-leave']) { + 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) { + for (var klass in runningAnimations) { animationsToCancel.push(runningAnimations[klass]); - cleanup(element, klass); } - runningAnimations = {}; - totalActiveAnimations = 0; + ngAnimateState = {}; + cleanup(element, true); } - } else if(lastAnimation.event == 'setClass') { + } else if (lastAnimation.event == 'setClass') { animationsToCancel.push(lastAnimation); cleanup(element, className); } - else if(runningAnimations[className]) { + else if (runningAnimations[className]) { var current = runningAnimations[className]; - if(current.event == animationEvent) { + if (current.event == animationEvent) { skipAnimation = true; } else { animationsToCancel.push(current); @@ -843,34 +1343,41 @@ angular.module('ngAnimate', ['ng']) } } - if(animationsToCancel.length > 0) { + if (animationsToCancel.length > 0) { forEach(animationsToCancel, function(operation) { operation.cancel(); }); } } - if(runner.isClassBased && !runner.isSetClassOperation && !skipAnimation) { + if (runner.isClassBased + && !runner.isSetClassOperation + && animationEvent != 'animate' + && !skipAnimation) { skipAnimation = (animationEvent == 'addClass') == element.hasClass(className); //opposite of XOR } - if(skipAnimation) { + if (skipAnimation) { + fireDOMOperation(); fireBeforeCallbackAsync(); fireAfterCallbackAsync(); fireDoneCallbackAsync(); - return; + return noopCancel; } - if(animationEvent == 'leave') { + 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) { + if (state) { var activeLeaveAnimation = state.active['ng-leave']; - if(activeLeaveAnimation) { + if (activeLeaveAnimation) { activeLeaveAnimation.cancel(); cleanup(element, 'ng-leave'); } @@ -880,17 +1387,22 @@ angular.module('ngAnimate', ['ng']) //the ng-animate class does nothing, but it's here to allow for //parent animations to find and cancel child animations when needed - element.addClass(NG_ANIMATE_CLASS_NAME); + $$jqLite.addClass(element, NG_ANIMATE_CLASS_NAME); + if (options && options.tempClasses) { + forEach(options.tempClasses, function(className) { + $$jqLite.addClass(element, className); + }); + } var localAnimationCount = globalAnimationCounter++; totalActiveAnimations++; runningAnimations[className] = runner; element.data(NG_ANIMATE_STATE, { - last : runner, - active : runningAnimations, - index : localAnimationCount, - totalActive : totalActiveAnimations + last: runner, + active: runningAnimations, + index: localAnimationCount, + totalActive: totalActiveAnimations }); //first we run the before animations and when all of those are complete @@ -903,7 +1415,7 @@ angular.module('ngAnimate', ['ng']) (runner.isClassBased && data.active[className].event != animationEvent); fireDOMOperation(); - if(cancelled === true) { + if (cancelled === true) { closeAnimation(); } else { fireAfterCallbackAsync(); @@ -911,13 +1423,15 @@ angular.module('ngAnimate', ['ng']) } }); + return runner.cancel; + function fireDOMCallback(animationPhase) { var eventName = '$animate:' + animationPhase; - if(elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { + if (elementEvents && elementEvents[eventName] && elementEvents[eventName].length > 0) { $$asyncCallback(function() { element.triggerHandler(eventName, { - event : animationEvent, - className : className + event: animationEvent, + className: className }); }); } @@ -933,37 +1447,44 @@ angular.module('ngAnimate', ['ng']) function fireDoneCallbackAsync() { fireDOMCallback('close'); - if(doneCallback) { - $$asyncCallback(function() { - doneCallback(); - }); - } + doneCallback(); } //it is less complicated to use a flag than managing and canceling //timeouts containing multiple callbacks. function fireDOMOperation() { - if(!fireDOMOperation.hasBeenRun) { + if (!fireDOMOperation.hasBeenRun) { fireDOMOperation.hasBeenRun = true; domOperation(); } } function closeAnimation() { - if(!closeAnimation.hasBeenRun) { + if (!closeAnimation.hasBeenRun) { + if (runner) { //the runner doesn't exist if it fails to instantiate + runner.applyStyles(); + } + closeAnimation.hasBeenRun = true; + if (options && options.tempClasses) { + forEach(options.tempClasses, function(className) { + $$jqLite.removeClass(element, className); + }); + } + var data = element.data(NG_ANIMATE_STATE); - if(data) { + if (data) { + /* only structural animations wait for reflow before removing an animation, but class-based animations don't. An example of this failing would be when a parent HTML tag has a ng-class attribute causing ALL directives below to skip animations during the digest */ - if(runner && runner.isClassBased) { + if (runner && runner.isClassBased) { cleanup(element, className); } else { $$asyncCallback(function() { var data = element.data(NG_ANIMATE_STATE) || {}; - if(localAnimationCount == data.index) { + if (localAnimationCount == data.index) { cleanup(element, className, animationEvent); } }); @@ -984,7 +1505,7 @@ angular.module('ngAnimate', ['ng']) forEach(nodes, function(element) { element = angular.element(element); var data = element.data(NG_ANIMATE_STATE); - if(data && data.active) { + if (data && data.active) { forEach(data.active, function(runner) { runner.cancel(); }); @@ -994,52 +1515,71 @@ angular.module('ngAnimate', ['ng']) } function cleanup(element, className) { - if(isMatchingElement(element, $rootElement)) { - if(!rootAnimateState.disabled) { + if (isMatchingElement(element, $rootElement)) { + if (!rootAnimateState.disabled) { rootAnimateState.running = false; rootAnimateState.structural = false; } - } else if(className) { + } else if (className) { var data = element.data(NG_ANIMATE_STATE) || {}; var removeAnimations = className === true; - if(!removeAnimations && data.active && data.active[className]) { + if (!removeAnimations && data.active && data.active[className]) { data.totalActive--; delete data.active[className]; } - if(removeAnimations || !data.totalActive) { - element.removeClass(NG_ANIMATE_CLASS_NAME); + if (removeAnimations || !data.totalActive) { + $$jqLite.removeClass(element, NG_ANIMATE_CLASS_NAME); element.removeData(NG_ANIMATE_STATE); } } } function animationsDisabled(element, parentElement) { - if (rootAnimateState.disabled) return true; + if (rootAnimateState.disabled) { + return true; + } - if(isMatchingElement(element, $rootElement)) { - return rootAnimateState.disabled || rootAnimateState.running; + if (isMatchingElement(element, $rootElement)) { + return rootAnimateState.running; } + var allowChildAnimations, parentRunningAnimation, hasParent; do { //the element did not reach the root element which means that it //is not apart of the DOM. Therefore there is no reason to do //any animations on it - if(parentElement.length === 0) break; + if (parentElement.length === 0) break; var isRoot = isMatchingElement(parentElement, $rootElement); - var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE); - var result = state && (!!state.disabled || state.running || state.totalActive > 0); - if(isRoot || result) { - return result; + 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(isRoot) return true; + //once a flag is found that is strictly false then everything before + //it will be discarded and all child animations will be restricted + if (allowChildAnimations !== false) { + var animateChildrenFlag = parentElement.data(NG_ANIMATE_CHILDREN); + if (angular.isDefined(animateChildrenFlag)) { + allowChildAnimations = animateChildrenFlag; + } + } + + parentRunningAnimation = parentRunningAnimation || + state.running || + (state.last && !state.last.isClassBased); } - while(parentElement = parentElement.parent()); + while (parentElement = parentElement.parent()); - return true; + return !hasParent || (!allowChildAnimations && parentRunningAnimation); } }]); @@ -1078,9 +1618,9 @@ angular.module('ngAnimate', ['ng']) 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 NG_ANIMATE_BLOCK_CLASS_NAME = 'ng-animate-block-transitions'; var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; var CLOSING_TIME_BUFFER = 1.5; var ONE_SECOND = 1000; @@ -1089,8 +1629,18 @@ angular.module('ngAnimate', ['ng']) var parentCounter = 0; var animationReflowQueue = []; var cancelAnimationReflow; + function clearCacheAfterReflow() { + if (!cancelAnimationReflow) { + cancelAnimationReflow = $$animateReflow(function() { + animationReflowQueue = []; + cancelAnimationReflow = null; + lookupCache = {}; + }); + } + } + function afterReflow(element, callback) { - if(cancelAnimationReflow) { + if (cancelAnimationReflow) { cancelAnimationReflow(); } animationReflowQueue.push(callback); @@ -1119,7 +1669,7 @@ angular.module('ngAnimate', ['ng']) //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) { + if (futureTimestamp <= closingTimestamp) { return; } @@ -1135,64 +1685,52 @@ angular.module('ngAnimate', ['ng']) function closeAllAnimations(elements) { forEach(elements, function(element) { var elementData = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(elementData) { - (elementData.closeAnimationFn || noop)(); + if (elementData) { + forEach(elementData.closeAnimationFns, function(fn) { + fn(); + }); } }); } function getElementAnimationDetails(element, cacheKey) { var data = cacheKey ? lookupCache[cacheKey] : null; - if(!data) { + if (!data) { var transitionDuration = 0; var transitionDelay = 0; var animationDuration = 0; var animationDelay = 0; - var transitionDelayStyle; - var animationDelayStyle; - var transitionDurationStyle; - var transitionPropertyStyle; //we want all the styles defined before and after forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; - transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; - + var transitionDurationStyle = elementStyles[TRANSITION_PROP + DURATION_KEY]; transitionDuration = Math.max(parseMaxTime(transitionDurationStyle), transitionDuration); - transitionPropertyStyle = elementStyles[TRANSITION_PROP + PROPERTY_KEY]; - - transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; - + var transitionDelayStyle = elementStyles[TRANSITION_PROP + DELAY_KEY]; transitionDelay = Math.max(parseMaxTime(transitionDelayStyle), transitionDelay); - animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; - - animationDelay = Math.max(parseMaxTime(animationDelayStyle), animationDelay); + var animationDelayStyle = elementStyles[ANIMATION_PROP + DELAY_KEY]; + animationDelay = Math.max(parseMaxTime(elementStyles[ANIMATION_PROP + DELAY_KEY]), animationDelay); var aDuration = parseMaxTime(elementStyles[ANIMATION_PROP + DURATION_KEY]); - if(aDuration > 0) { + if (aDuration > 0) { aDuration *= parseInt(elementStyles[ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY], 10) || 1; } - animationDuration = Math.max(aDuration, animationDuration); } }); data = { - total : 0, - transitionPropertyStyle: transitionPropertyStyle, - transitionDurationStyle: transitionDurationStyle, - transitionDelayStyle: transitionDelayStyle, + total: 0, transitionDelay: transitionDelay, transitionDuration: transitionDuration, - animationDelayStyle: animationDelayStyle, animationDelay: animationDelay, animationDuration: animationDuration }; - if(cacheKey) { + if (cacheKey) { lookupCache[cacheKey] = data; } } @@ -1201,7 +1739,7 @@ angular.module('ngAnimate', ['ng']) function parseMaxTime(str) { var maxValue = 0; - var values = angular.isString(str) ? + var values = isString(str) ? str.split(/\s*,\s*/) : []; forEach(values, function(value) { @@ -1213,179 +1751,187 @@ angular.module('ngAnimate', ['ng']) function getCacheKey(element) { var parentElement = element.parent(); var parentID = parentElement.data(NG_ANIMATE_PARENT_KEY); - if(!parentID) { + if (!parentID) { parentElement.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); parentID = parentCounter; } return parentID + '-' + extractElementNode(element).getAttribute('class'); } - function animateSetup(animationEvent, element, className, calculationDecorator) { + 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) { + if (itemIndex > 0) { var staggerClassName = className + '-stagger'; var staggerCacheKey = cacheKey + ' ' + staggerClassName; var applyClasses = !lookupCache[staggerCacheKey]; - applyClasses && element.addClass(staggerClassName); + applyClasses && $$jqLite.addClass(element, staggerClassName); stagger = getElementAnimationDetails(element, staggerCacheKey); - applyClasses && element.removeClass(staggerClassName); + applyClasses && $$jqLite.removeClass(element, staggerClassName); } - /* the animation itself may need to add/remove special CSS classes - * before calculating the anmation styles */ - calculationDecorator = calculationDecorator || - function(fn) { return fn(); }; - - element.addClass(className); + $$jqLite.addClass(element, className); var formerData = element.data(NG_ANIMATE_CSS_DATA_KEY) || {}; - - var timings = calculationDecorator(function() { - return getElementAnimationDetails(element, eventCacheKey); - }); - + var timings = getElementAnimationDetails(element, eventCacheKey); var transitionDuration = timings.transitionDuration; var animationDuration = timings.animationDuration; - if(transitionDuration === 0 && animationDuration === 0) { - element.removeClass(className); + + if (structural && transitionDuration === 0 && animationDuration === 0) { + $$jqLite.removeClass(element, className); return false; } + var blockTransition = styles || (structural && transitionDuration > 0); + var blockAnimation = animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + + var closeAnimationFns = formerData.closeAnimationFns || []; element.data(NG_ANIMATE_CSS_DATA_KEY, { - running : formerData.running || 0, - itemIndex : itemIndex, - stagger : stagger, - timings : timings, - closeAnimationFn : noop + stagger: stagger, + cacheKey: eventCacheKey, + running: formerData.running || 0, + itemIndex: itemIndex, + blockTransition: blockTransition, + closeAnimationFns: closeAnimationFns }); - //temporarily disable the transition so that the enter styles - //don't animate twice (this is here to avoid a bug in Chrome/FF). - var isCurrentlyAnimating = formerData.running > 0 || animationEvent == 'setClass'; - if(transitionDuration > 0) { - blockTransitions(element, className, isCurrentlyAnimating); - } - - //staggering keyframe animations work by adjusting the `animation-delay` CSS property - //on the given element, however, the delay value can only calculated after the reflow - //since by that time $animate knows how many elements are being animated. Therefore, - //until the reflow occurs the element needs to be blocked (where the keyframe animation - //is set to `none 0s`). This blocking mechanism should only be set for when a stagger - //animation is detected and when the element item index is greater than 0. - if(animationDuration > 0 && stagger.animationDelay > 0 && stagger.animationDuration === 0) { - blockKeyframeAnimations(element); - } - - return true; - } - - function isStructuralAnimation(className) { - return className == 'ng-enter' || className == 'ng-move' || className == 'ng-leave'; - } + var node = extractElementNode(element); - function blockTransitions(element, className, isAnimating) { - if(isStructuralAnimation(className) || !isAnimating) { - extractElementNode(element).style[TRANSITION_PROP + PROPERTY_KEY] = 'none'; - } else { - element.addClass(NG_ANIMATE_BLOCK_CLASS_NAME); + if (blockTransition) { + blockTransitions(node, true); + if (styles) { + element.css(styles); + } } - } - function blockKeyframeAnimations(element) { - extractElementNode(element).style[ANIMATION_PROP] = 'none 0s'; - } - - function unblockTransitions(element, className) { - var prop = TRANSITION_PROP + PROPERTY_KEY; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; + if (blockAnimation) { + blockAnimations(node, true); } - element.removeClass(NG_ANIMATE_BLOCK_CLASS_NAME); - } - function unblockKeyframeAnimations(element) { - var prop = ANIMATION_PROP; - var node = extractElementNode(element); - if(node.style[prop] && node.style[prop].length > 0) { - node.style[prop] = ''; - } + return true; } - function animateRun(animationEvent, element, className, activeAnimationComplete) { + 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) { + if (node.getAttribute('class').indexOf(className) == -1 || !elementData) { activeAnimationComplete(); return; } var activeClassName = ''; + var pendingClassName = ''; forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; + var prefix = (i > 0 ? ' ' : '') + klass; + activeClassName += prefix + '-active'; + pendingClassName += prefix + '-pending'; }); - var stagger = elementData.stagger; - var timings = elementData.timings; + var style = ''; + var appliedStyles = []; var itemIndex = elementData.itemIndex; - var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); - var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); - var maxDelayTime = maxDelay * ONE_SECOND; - - var startTime = Date.now(); - var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + 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 style = '', appliedStyles = []; - if(timings.transitionDuration > 0) { - var propertyStyle = timings.transitionPropertyStyle; - if(propertyStyle.indexOf('all') == -1) { - style += CSS_PREFIX + 'transition-property: ' + propertyStyle + ';'; - style += CSS_PREFIX + 'transition-duration: ' + timings.transitionDurationStyle + ';'; - appliedStyles.push(CSS_PREFIX + 'transition-property'); - appliedStyles.push(CSS_PREFIX + 'transition-duration'); + var animationStaggerDelay = 0; + if (stagger.animationDelay > 0 && stagger.animationDuration === 0) { + animationStaggerDelay = stagger.animationDelay * itemIndex; + appliedStyles.push(CSS_PREFIX + 'animation-play-state'); } + + staggerTime = Math.round(Math.max(transitionStaggerDelay, animationStaggerDelay) * 100) / 100; } - if(itemIndex > 0) { - if(stagger.transitionDelay > 0 && stagger.transitionDuration === 0) { - var delayStyle = timings.transitionDelayStyle; - style += CSS_PREFIX + 'transition-delay: ' + - prepareStaggerDelay(delayStyle, stagger.transitionDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'transition-delay'); + if (!staggerTime) { + $$jqLite.addClass(element, activeClassName); + if (elementData.blockTransition) { + blockTransitions(node, false); } + } + + var eventCacheKey = elementData.cacheKey + ' ' + activeClassName; + var timings = getElementAnimationDetails(element, eventCacheKey); + var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration); + if (maxDuration === 0) { + $$jqLite.removeClass(element, activeClassName); + animateClose(element, className); + activeAnimationComplete(); + return; + } - if(stagger.animationDelay > 0 && stagger.animationDuration === 0) { - style += CSS_PREFIX + 'animation-delay: ' + - prepareStaggerDelay(timings.animationDelayStyle, stagger.animationDelay, itemIndex) + '; '; - appliedStyles.push(CSS_PREFIX + 'animation-delay'); + if (!staggerTime && styles) { + if (!timings.transitionDuration) { + element.css('transition', timings.animationDuration + 's linear all'); + appliedStyles.push('transition'); } + element.css(styles); } - if(appliedStyles.length > 0) { + var maxDelay = Math.max(timings.transitionDelay, timings.animationDelay); + var maxDelayTime = maxDelay * ONE_SECOND; + + if (appliedStyles.length > 0) { //the element being animated may sometimes contain comment nodes in //the jqLite object, so we're safe to use a single variable to house //the styles since there is always only one element being animated var oldStyle = node.getAttribute('style') || ''; + if (oldStyle.charAt(oldStyle.length - 1) !== ';') { + oldStyle += ';'; + } node.setAttribute('style', oldStyle + ' ' + style); } + var startTime = Date.now(); + var css3AnimationEvents = ANIMATIONEND_EVENT + ' ' + TRANSITIONEND_EVENT; + var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; + var totalTime = (staggerTime + animationTime) * ONE_SECOND; + + var staggerTimeout; + if (staggerTime > 0) { + $$jqLite.addClass(element, pendingClassName); + staggerTimeout = $timeout(function() { + staggerTimeout = null; + + if (timings.transitionDuration > 0) { + blockTransitions(node, false); + } + if (timings.animationDuration > 0) { + blockAnimations(node, false); + } + + $$jqLite.addClass(element, activeClassName); + $$jqLite.removeClass(element, pendingClassName); + + if (styles) { + if (timings.transitionDuration === 0) { + element.css('transition', timings.animationDuration + 's linear all'); + } + element.css(styles); + appliedStyles.push('transition'); + } + }, staggerTime * ONE_SECOND, false); + } + element.on(css3AnimationEvents, onAnimationProgress); - element.addClass(activeClassName); - elementData.closeAnimationFn = function() { + elementData.closeAnimationFns.push(function() { onEnd(); activeAnimationComplete(); - }; - - var staggerTime = itemIndex * (Math.max(stagger.animationDelay, stagger.transitionDelay) || 0); - var animationTime = (maxDelay + maxDuration) * CLOSING_TIME_BUFFER; - var totalTime = (staggerTime + animationTime) * ONE_SECOND; + }); elementData.running++; animationCloseHandler(element, totalTime); @@ -1394,9 +1940,13 @@ angular.module('ngAnimate', ['ng']) // This will automatically be called by $animate so // there is no need to attach this internally to the // timeout done method. - function onEnd(cancelled) { + function onEnd() { element.off(css3AnimationEvents, onAnimationProgress); - element.removeClass(activeClassName); + $$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) { @@ -1420,44 +1970,44 @@ angular.module('ngAnimate', ['ng']) * 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) { + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { activeAnimationComplete(); } } } - function prepareStaggerDelay(delayStyle, staggerDelay, index) { - var style = ''; - forEach(delayStyle.split(','), function(val, i) { - style += (i > 0 ? ',' : '') + - (index * staggerDelay + parseInt(val, 10)) + 's'; - }); - return style; + function blockTransitions(node, bool) { + node.style[TRANSITION_PROP + PROPERTY_KEY] = bool ? 'none' : ''; + } + + function blockAnimations(node, bool) { + node.style[ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY] = bool ? 'paused' : ''; } - function animateBefore(animationEvent, element, className, calculationDecorator) { - if(animateSetup(animationEvent, element, className, calculationDecorator)) { + function animateBefore(animationEvent, element, className, styles) { + if (animateSetup(animationEvent, element, className, styles)) { return function(cancelled) { cancelled && animateClose(element, className); }; } } - function animateAfter(animationEvent, element, className, afterAnimationComplete) { - if(element.data(NG_ANIMATE_CSS_DATA_KEY)) { - return animateRun(animationEvent, element, className, afterAnimationComplete); + function animateAfter(animationEvent, element, className, afterAnimationComplete, styles) { + if (element.data(NG_ANIMATE_CSS_DATA_KEY)) { + return animateRun(animationEvent, element, className, afterAnimationComplete, styles); } else { animateClose(element, className); afterAnimationComplete(); } } - function animate(animationEvent, element, className, animationComplete) { + 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); - if(!preReflowCancellation) { + var preReflowCancellation = animateBefore(animationEvent, element, className, options.from); + if (!preReflowCancellation) { + clearCacheAfterReflow(); animationComplete(); return; } @@ -1469,12 +2019,10 @@ angular.module('ngAnimate', ['ng']) //happen in the first place var cancel = preReflowCancellation; afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); //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); + cancel = animateAfter(animationEvent, element, className, animationComplete, options.to); }); return function(cancelled) { @@ -1483,127 +2031,100 @@ angular.module('ngAnimate', ['ng']) } function animateClose(element, className) { - element.removeClass(className); + $$jqLite.removeClass(element, className); var data = element.data(NG_ANIMATE_CSS_DATA_KEY); - if(data) { - if(data.running) { + if (data) { + if (data.running) { data.running--; } - if(!data.running || data.running === 0) { + if (!data.running || data.running === 0) { element.removeData(NG_ANIMATE_CSS_DATA_KEY); } } } return { - enter : function(element, animationCompleted) { - return animate('enter', element, 'ng-enter', animationCompleted); + animate: function(element, className, from, to, animationCompleted, options) { + options = options || {}; + options.from = from; + options.to = to; + return animate('animate', element, className, animationCompleted, options); }, - leave : function(element, animationCompleted) { - return animate('leave', element, 'ng-leave', animationCompleted); + enter: function(element, animationCompleted, options) { + options = options || {}; + return animate('enter', element, 'ng-enter', animationCompleted, options); }, - move : function(element, animationCompleted) { - return animate('move', element, 'ng-move', animationCompleted); + leave: function(element, animationCompleted, options) { + options = options || {}; + return animate('leave', element, 'ng-leave', animationCompleted, options); }, - beforeSetClass : function(element, add, remove, animationCompleted) { + move: function(element, animationCompleted, options) { + options = options || {}; + return animate('move', element, 'ng-move', animationCompleted, options); + }, + + beforeSetClass: function(element, add, remove, animationCompleted, options) { + options = options || {}; var className = suffixClasses(remove, '-remove') + ' ' + suffixClasses(add, '-add'); - var cancellationMethod = animateBefore('setClass', element, className, function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(remove); - element.addClass(add); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + var cancellationMethod = animateBefore('setClass', element, className, options.from); + if (cancellationMethod) { + afterReflow(element, animationCompleted); return cancellationMethod; } + clearCacheAfterReflow(); animationCompleted(); }, - beforeAddClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), function(fn) { - - /* when a CSS class is added to an element then the transition style that - * is applied is the transition defined on the element when the CSS class - * is added at the time of the animation. This is how CSS3 functions - * outside of ngAnimate. */ - element.addClass(className); - var timings = fn(); - element.removeClass(className); - return timings; - }); + beforeAddClass: function(element, className, animationCompleted, options) { + options = options || {}; + var cancellationMethod = animateBefore('addClass', element, suffixClasses(className, '-add'), options.from); + if (cancellationMethod) { + afterReflow(element, animationCompleted); + return cancellationMethod; + } + clearCacheAfterReflow(); + animationCompleted(); + }, - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); + beforeRemoveClass: function(element, className, animationCompleted, options) { + options = options || {}; + var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), options.from); + if (cancellationMethod) { + afterReflow(element, animationCompleted); return cancellationMethod; } + clearCacheAfterReflow(); animationCompleted(); }, - setClass : function(element, add, remove, animationCompleted) { + 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); - }, - - addClass : function(element, className, animationCompleted) { - return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted); + return animateAfter('setClass', element, className, animationCompleted, options.to); }, - beforeRemoveClass : function(element, className, animationCompleted) { - var cancellationMethod = animateBefore('removeClass', element, suffixClasses(className, '-remove'), function(fn) { - /* when classes are removed from an element then the transition style - * that is applied is the transition defined on the element without the - * CSS class being there. This is how CSS3 functions outside of ngAnimate. - * http://plnkr.co/edit/j8OzgTNxHTb4n3zLyjGW?p=preview */ - var klass = element.attr('class'); - element.removeClass(className); - var timings = fn(); - element.attr('class', klass); - return timings; - }); - - if(cancellationMethod) { - afterReflow(element, function() { - unblockTransitions(element, className); - unblockKeyframeAnimations(element); - animationCompleted(); - }); - return cancellationMethod; - } - animationCompleted(); + addClass: function(element, className, animationCompleted, options) { + options = options || {}; + return animateAfter('addClass', element, suffixClasses(className, '-add'), animationCompleted, options.to); }, - removeClass : function(element, className, animationCompleted) { - return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted); + removeClass: function(element, className, animationCompleted, options) { + options = options || {}; + return animateAfter('removeClass', element, suffixClasses(className, '-remove'), animationCompleted, options.to); } }; function suffixClasses(classes, suffix) { var className = ''; - classes = angular.isArray(classes) ? classes : classes.split(/\s+/); + classes = isArray(classes) ? classes : classes.split(/\s+/); forEach(classes, function(klass, i) { - if(klass && klass.length > 0) { + if (klass && klass.length > 0) { className += (i > 0 ? ' ' : '') + klass + suffix; } }); diff --git a/xstatic/pkg/angular/data/angular-aria.js b/xstatic/pkg/angular/data/angular-aria.js new file mode 100644 index 0000000..197b9c8 --- /dev/null +++ b/xstatic/pkg/angular/data/angular-aria.js @@ -0,0 +1,332 @@ +/** + * @license AngularJS v1.3.7 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/** + * @ngdoc module + * @name ngAria + * @description + * + * The `ngAria` module provides support for common + * [<abbr title="Accessible Rich Internet Applications">ARIA</abbr>](http://www.w3.org/TR/wai-aria/) + * attributes that convey state or semantic information about the application for users + * of assistive technologies, such as screen readers. + * + * <div doc-module-components="ngAria"></div> + * + * ## Usage + * + * For ngAria to do its magic, simply include the module as a dependency. The directives supported + * by ngAria are: + * `ngModel`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, `ngDblClick`, and `ngMessages`. + * + * Below is a more detailed breakdown of the attributes handled by ngAria: + * + * | Directive | Supported Attributes | + * |---------------------------------------------|----------------------------------------------------------------------------------------| + * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required | + * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled | + * | {@link ng.directive:ngShow ngShow} | aria-hidden | + * | {@link ng.directive:ngHide ngHide} | aria-hidden | + * | {@link ng.directive:ngClick ngClick} | tabindex, keypress event | + * | {@link ng.directive:ngDblclick ngDblclick} | tabindex | + * | {@link module:ngMessages ngMessages} | aria-live | + * + * Find out more information about each directive by reading the + * {@link guide/accessibility ngAria Developer Guide}. + * + * ##Example + * Using ngDisabled with ngAria: + * ```html + * <md-checkbox ng-disabled="disabled"> + * ``` + * Becomes: + * ```html + * <md-checkbox ng-disabled="disabled" aria-disabled="true"> + * ``` + * + * ##Disabling Attributes + * It's possible to disable individual attributes added by ngAria with the + * {@link ngAria.$ariaProvider#config config} method. For more details, see the + * {@link guide/accessibility Developer Guide}. + */ + /* global -ngAriaModule */ +var ngAriaModule = angular.module('ngAria', ['ng']). + provider('$aria', $AriaProvider); + +/** + * @ngdoc provider + * @name $ariaProvider + * + * @description + * + * Used for configuring the ARIA attributes injected and managed by ngAria. + * + * ```js + * angular.module('myApp', ['ngAria'], function config($ariaProvider) { + * $ariaProvider.config({ + * ariaValue: true, + * tabindex: false + * }); + * }); + *``` + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + * + */ +function $AriaProvider() { + var config = { + ariaHidden: true, + ariaChecked: true, + ariaDisabled: true, + ariaRequired: true, + ariaInvalid: true, + ariaMultiline: true, + ariaValue: true, + tabindex: true, + bindKeypress: true + }; + + /** + * @ngdoc method + * @name $ariaProvider#config + * + * @param {object} config object to enable/disable specific ARIA attributes + * + * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags + * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags + * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags + * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags + * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags + * - **ariaMultiline** – `{boolean}` – Enables/disables aria-multiline tags + * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags + * - **tabindex** – `{boolean}` – Enables/disables tabindex tags + * - **bindKeypress** – `{boolean}` – Enables/disables keypress event binding on ng-click + * + * @description + * Enables/disables various ARIA attributes + */ + this.config = function(newConfig) { + config = angular.extend(config, newConfig); + }; + + function watchExpr(attrName, ariaAttr, negate) { + return function(scope, elem, attr) { + var ariaCamelName = attr.$normalize(ariaAttr); + if (config[ariaCamelName] && !attr[ariaCamelName]) { + scope.$watch(attr[attrName], function(boolVal) { + if (negate) { + boolVal = !boolVal; + } + elem.attr(ariaAttr, boolVal); + }); + } + }; + } + + /** + * @ngdoc service + * @name $aria + * + * @description + * + * The $aria service contains helper methods for applying common + * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives. + * + * ngAria injects common accessibility attributes that tell assistive technologies when HTML + * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria, + * let's review a code snippet from ngAria itself: + * + *```js + * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) { + * return $aria.$$watchExpr('ngDisabled', 'aria-disabled'); + * }]) + *``` + * Shown above, the ngAria module creates a directive with the same signature as the + * traditional `ng-disabled` directive. But this ngAria version is dedicated to + * solely managing accessibility attributes. The internal `$aria` service is used to watch the + * boolean attribute `ngDisabled`. If it has not been explicitly set by the developer, + * `aria-disabled` is injected as an attribute with its value synchronized to the value in + * `ngDisabled`. + * + * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do + * anything to enable this feature. The `aria-disabled` attribute is automatically managed + * simply as a silent side-effect of using `ng-disabled` with the ngAria module. + * + * The full list of directives that interface with ngAria: + * * **ngModel** + * * **ngShow** + * * **ngHide** + * * **ngClick** + * * **ngDblclick** + * * **ngMessages** + * * **ngDisabled** + * + * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each + * directive. + * + * + * ## Dependencies + * Requires the {@link ngAria} module to be installed. + */ + this.$get = function() { + return { + config: function(key) { + return config[key]; + }, + $$watchExpr: watchExpr + }; + }; +} + + +ngAriaModule.directive('ngShow', ['$aria', function($aria) { + return $aria.$$watchExpr('ngShow', 'aria-hidden', true); +}]) +.directive('ngHide', ['$aria', function($aria) { + return $aria.$$watchExpr('ngHide', 'aria-hidden', false); +}]) +.directive('ngModel', ['$aria', function($aria) { + + function shouldAttachAttr(attr, normalizedAttr, elem) { + return $aria.config(normalizedAttr) && !elem.attr(attr); + } + + function getShape(attr, elem) { + var type = attr.type, + role = attr.role; + + return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' : + ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' : + (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : + (type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : ''; + } + + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elem, attr, ngModel) { + var shape = getShape(attr, elem); + var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem); + + function ngAriaWatchModelValue() { + return ngModel.$modelValue; + } + + function getRadioReaction() { + if (needsTabIndex) { + needsTabIndex = false; + return function ngAriaRadioReaction(newVal) { + var boolVal = newVal === attr.value; + elem.attr('aria-checked', boolVal); + elem.attr('tabindex', 0 - !boolVal); + }; + } else { + return function ngAriaRadioReaction(newVal) { + elem.attr('aria-checked', newVal === attr.value); + }; + } + } + + function ngAriaCheckboxReaction(newVal) { + elem.attr('aria-checked', !!newVal); + } + + switch (shape) { + case 'radio': + case 'checkbox': + if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) { + scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? + getRadioReaction() : ngAriaCheckboxReaction); + } + break; + case 'range': + if ($aria.config('ariaValue')) { + if (attr.min && !elem.attr('aria-valuemin')) { + elem.attr('aria-valuemin', attr.min); + } + if (attr.max && !elem.attr('aria-valuemax')) { + elem.attr('aria-valuemax', attr.max); + } + if (!elem.attr('aria-valuenow')) { + scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { + elem.attr('aria-valuenow', newVal); + }); + } + } + break; + case 'multiline': + if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) { + elem.attr('aria-multiline', true); + } + break; + } + + if (needsTabIndex) { + elem.attr('tabindex', 0); + } + + if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) { + scope.$watch(function ngAriaRequiredWatch() { + return ngModel.$error.required; + }, function ngAriaRequiredReaction(newVal) { + elem.attr('aria-required', !!newVal); + }); + } + + if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) { + scope.$watch(function ngAriaInvalidWatch() { + return ngModel.$invalid; + }, function ngAriaInvalidReaction(newVal) { + elem.attr('aria-invalid', !!newVal); + }); + } + } + }; +}]) +.directive('ngDisabled', ['$aria', function($aria) { + return $aria.$$watchExpr('ngDisabled', 'aria-disabled'); +}]) +.directive('ngMessages', function() { + return { + restrict: 'A', + require: '?ngMessages', + link: function(scope, elem, attr, ngMessages) { + if (!elem.attr('aria-live')) { + elem.attr('aria-live', 'assertive'); + } + } + }; +}) +.directive('ngClick',['$aria', function($aria) { + return { + restrict: 'A', + link: function(scope, elem, attr) { + if ($aria.config('tabindex') && !elem.attr('tabindex')) { + elem.attr('tabindex', 0); + } + + if ($aria.config('bindKeypress') && !elem.attr('ng-keypress')) { + elem.on('keypress', function(event) { + if (event.keyCode === 32 || event.keyCode === 13) { + scope.$eval(attr.ngClick); + } + }); + } + } + }; +}]) +.directive('ngDblclick', ['$aria', function($aria) { + return function(scope, elem, attr) { + if ($aria.config('tabindex') && !elem.attr('tabindex')) { + elem.attr('tabindex', 0); + } + }; +}]); + + +})(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-cookies.js b/xstatic/pkg/angular/data/angular-cookies.js index f43d44d..d80d9d9 100644 --- a/xstatic/pkg/angular/data/angular-cookies.js +++ b/xstatic/pkg/angular/data/angular-cookies.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -37,20 +37,18 @@ angular.module('ngCookies', ['ng']). * Requires the {@link ngCookies `ngCookies`} module to be installed. * * @example - <example> - <file name="index.html"> - <script> - function ExampleController($cookies) { - // Retrieving a cookie - var favoriteCookie = $cookies.myFavorite; - // Setting a cookie - $cookies.myFavorite = 'oatmeal'; - } - </script> - </file> - </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) { + factory('$cookies', ['$rootScope', '$browser', function($rootScope, $browser) { var cookies = {}, lastCookies = {}, lastBrowserCookies, @@ -97,7 +95,7 @@ angular.module('ngCookies', ['ng']). } //update all cookies updated in $cookies - for(name in cookies) { + for (name in cookies) { value = cookies[name]; if (!angular.isString(value)) { value = '' + value; @@ -110,7 +108,7 @@ angular.module('ngCookies', ['ng']). } //verify what was actually stored - if (updated){ + if (updated) { updated = false; browserCookies = $browser.cookies(); @@ -143,6 +141,18 @@ angular.module('ngCookies', ['ng']). * 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) { diff --git a/xstatic/pkg/angular/data/angular-csp.css b/xstatic/pkg/angular/data/angular-csp.css index 0d3d3a9..0ce9d86 100644 --- a/xstatic/pkg/angular/data/angular-csp.css +++ b/xstatic/pkg/angular/data/angular-csp.css @@ -4,15 +4,10 @@ [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak, -.ng-hide { +.ng-hide:not(.ng-hide-animate) { display: none !important; } ng\:form { display: block; } - -.ng-animate-block-transitions { - transition:0s all!important; - -webkit-transition:0s all!important; -} diff --git a/xstatic/pkg/angular/data/angular-loader.js b/xstatic/pkg/angular/data/angular-loader.js index 7bf81bb..2ef986c 100644 --- a/xstatic/pkg/angular/data/angular-loader.js +++ b/xstatic/pkg/angular/data/angular-loader.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -31,52 +31,37 @@ * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ -function minErr(module) { - return function () { +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { var code = arguments[0], prefix = '[' + (module ? module + ':' : '') + code + '] ', template = arguments[1], templateArgs = arguments, - stringify = function (obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return JSON.stringify(obj); - } - return obj; - }, + message, i; - message = prefix + template.replace(/\{\d+\}/g, function (match) { + message = prefix + template.replace(/\{\d+\}/g, function(match) { var index = +match.slice(1, -1), arg; if (index + 2 < templateArgs.length) { - arg = templateArgs[index + 2]; - if (typeof arg === 'function') { - return arg.toString().replace(/ ?\{[\s\S]*$/, ''); - } else if (typeof arg === 'undefined') { - return 'undefined'; - } else if (typeof arg !== 'string') { - return toJson(arg); - } - return arg; + return toDebugString(templateArgs[index + 2]); } return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.16/' + + message = message + '\nhttp://errors.angularjs.org/1.3.7/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + - encodeURIComponent(stringify(arguments[i])); + message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + + encodeURIComponent(toDebugString(arguments[i])); } - - return new Error(message); + return new ErrorConstructor(message); }; } @@ -124,7 +109,7 @@ function setupModuleLoader(window) { * * # Module * - * A module is a collection of services, directives, filters, and configuration information. + * A module is a collection of services, directives, controllers, filters, and configuration information. * `angular.module` is used to configure the {@link auto.$injector $injector}. * * ```js @@ -152,9 +137,9 @@ function setupModuleLoader(window) { * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. -<<<<<* @param {!Array.<string>=} requires If specified then new module is being created. If ->>>>>* unspecified then the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as + * @param {!Array.<string>=} requires If specified then new module is being created. If + * 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. */ @@ -180,21 +165,25 @@ function setupModuleLoader(window) { var invokeQueue = []; /** @type {!Array.<Function>} */ + var configBlocks = []; + + /** @type {!Array.<Function>} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, /** * @ngdoc property * @name angular.Module#requires * @module ng - * @returns {Array.<string>} List of module names which must be loaded before this module. + * * @description * Holds the list of modules which the injector will load before the current module is * loaded. @@ -205,8 +194,9 @@ function setupModuleLoader(window) { * @ngdoc property * @name angular.Module#name * @module ng - * @returns {string} Name of the module. + * * @description + * Name of the module. */ name: name, @@ -297,7 +287,7 @@ function setupModuleLoader(window) { * }) * ``` * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ animation: invokeLater('$animateProvider', 'register'), @@ -346,6 +336,8 @@ function setupModuleLoader(window) { * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. */ config: config, @@ -369,7 +361,7 @@ function setupModuleLoader(window) { config(configFn); } - return moduleInstance; + return moduleInstance; /** * @param {string} provider @@ -377,9 +369,10 @@ function setupModuleLoader(window) { * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } diff --git a/xstatic/pkg/angular/data/angular-messages.js b/xstatic/pkg/angular/data/angular-messages.js new file mode 100644 index 0000000..b9cfedb --- /dev/null +++ b/xstatic/pkg/angular/data/angular-messages.js @@ -0,0 +1,400 @@ +/** + * @license AngularJS v1.3.7 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) {'use strict'; + +/** + * @ngdoc module + * @name ngMessages + * @description + * + * The `ngMessages` module provides enhanced support for displaying messages within templates + * (typically within forms or when rendering message objects that return key/value data). + * Instead of relying on JavaScript code and/or complex ng-if statements within your form template to + * show and hide error messages specific to the state of an input field, the `ngMessages` and + * `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. + * + * # 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. + * + * ```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> + * </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: + * + * ```javascript + * <!-- keep in mind that ngModel automatically sets these error flags --> + * myField.$error = { minlength : true, required : false }; + * ``` + * + * 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. + * + * 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. + * + * ```html + * <div ng-messages="myForm.myField.$error" ng-messages-multiple>...</div> + * ``` + * + * ## Reusing and Overriding Messages + * In addition to prioritization, ngMessages also allows for including messages from a remote or an inline + * template. This allows for generic collection of messages to be reused across multiple parts of an + * application. + * + * ```html + * <script type="text/ng-template" id="error-messages"> + * <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> + * ``` + * + * 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. + * + * ```html + * <!-- a generic template of error messages known as "my-custom-messages" --> + * <script type="text/ng-template" id="my-custom-messages"> + * <div ng-message="required">This field is required</div> + * <div ng-message="minlength">This field is too short</div> + * </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"> + * <!-- 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> + * </div> + * </form> + * ``` + * + * In the example HTML code above the message that is set on required will override the corresponding + * required message defined within the remote template. Therefore, with particular input fields (such + * 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. + * + * ## 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. + * + * 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 + * 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-message="alert" class="some-message">...</div> + * <div ng-message="fail" class="some-message">...</div> + * </div> + * ``` + * + * Then the CSS animation code for the message container looks like so: + * + * ```css + * .my-messages { + * transition:1s linear all; + * } + * .my-messages.ng-active { + * // messages are visible + * } + * .my-messages.ng-inactive { + * // messages are hidden + * } + * ``` + * + * Whenever an inner message is attached (becomes visible) or removed (becomes hidden) then the enter + * and leave animation is triggered for each particular element bound to the `ngMessage` directive. + * + * Therefore, the CSS code for the inner messages looks like so: + * + * ```css + * .some-message { + * transition:1s linear all; + * } + * + * .some-message.ng-enter {} + * .some-message.ng-enter.ng-enter-active {} + * + * .some-message.ng-leave {} + * .some-message.ng-leave.ng-leave-active {} + * ``` + * + * {@link ngAnimate Click here} to learn how to use JavaScript animations or to learn more about ngAnimate. + */ +angular.module('ngMessages', []) + + /** + * @ngdoc directive + * @module ngMessages + * @name ngMessages + * @restrict AE + * + * @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 + * 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.) + * + * A remote template can also be used to promote message reuseability and messages can also be + * overridden. + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @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> + * + * <!-- 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> + * ``` + * + * @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" + * deps="angular-messages.js" + * 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 /> + * + * <pre>myForm.myName.$error = {{ myForm.myName.$error | json }}</pre> + * + * <div ng-messages="myForm.myName.$error" style="color:maroon"> + * <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> + * </div> + * </form> + * </file> + * <file name="script.js"> + * angular.module('ngMessagesExample', ['ngMessages']); + * </file> + * </example> + */ + .directive('ngMessages', ['$compile', '$animate', '$templateRequest', + function($compile, $animate, $templateRequest) { + 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) + }; + + this.renderMessages = function(values, multiple) { + values = values || {}; + + var found; + angular.forEach(messages, function(message) { + if ((!found || multiple) && truthyVal(values[message.type])) { + message.attach(); + found = true; + } else { + message.detach(); + } + }); + + this.renderElementClasses(found); + + 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); + }; + + //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); + + var cachedValues, watchAttr = $attrs.ngMessages || $attrs['for']; //for is a reserved keyword + $scope.$watchCollection(watchAttr, function(values) { + cachedValues = values; + ctrl.renderMessages(values, multiple); + }); + + 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); + }); + } + } + }; + }]) + + + /** + * @ngdoc directive + * @name ngMessage + * @restrict AE + * @scope + * + * @description + * `ngMessage` is a directive with the purpose to show and hide a particular message. + * For `ngMessage` 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> + * + * <!-- 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> + * ``` + * + * {@link module:ngMessages Click here} to learn more about `ngMessages` and `ngMessage`. + * + * @param {string} ngMessage a string value corresponding to the message key. + */ + .directive('ngMessage', ['$animate', function($animate) { + var COMMENT_NODE = 8; + return { + require: '^ngMessages', + 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++; + } + } + + ngMessages.registerMessage(index, { + type: $attrs.ngMessage || $attrs.when, + attach: function() { + if (!element) { + $transclude($scope, function(clone) { + $animate.enter(clone, null, $element); + element = clone; + }); + } + }, + detach: function(now) { + if (element) { + $animate.leave(element); + element = null; + } + } + }); + } + }; + }]); + + +})(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-mocks.js b/xstatic/pkg/angular/data/angular-mocks.js index da804b4..3430bb7 100644 --- a/xstatic/pkg/angular/data/angular-mocks.js +++ b/xstatic/pkg/angular/data/angular-mocks.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -53,9 +53,10 @@ angular.mock.$Browser = function() { self.onUrlChange = function(listener) { self.pollFns.push( function() { - if (self.$$lastUrl != self.$$url) { + if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) { self.$$lastUrl = self.$$url; - listener(self.$$url); + self.$$lastState = self.$$state; + listener(self.$$url, self.$$state); } } ); @@ -63,6 +64,8 @@ angular.mock.$Browser = function() { return listener; }; + self.$$checkUrlChange = angular.noop; + self.cookieHash = {}; self.lastCookieHash = {}; self.deferredFns = []; @@ -71,7 +74,7 @@ angular.mock.$Browser = function() { self.defer = function(fn, delay) { delay = delay || 0; self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); - self.deferredFns.sort(function(a,b){ return a.time - b.time;}); + self.deferredFns.sort(function(a, b) { return a.time - b.time;}); return self.deferredNextId++; }; @@ -114,7 +117,7 @@ angular.mock.$Browser = function() { self.defer.now += delay; } else { if (self.deferredFns.length) { - self.defer.now = self.deferredFns[self.deferredFns.length-1].time; + self.defer.now = self.deferredFns[self.deferredFns.length - 1].time; } else { throw new Error('No deferred tasks to be flushed'); } @@ -125,7 +128,7 @@ angular.mock.$Browser = function() { } }; - self.$$baseHref = ''; + self.$$baseHref = '/'; self.baseHref = function() { return this.$$baseHref; }; @@ -139,7 +142,7 @@ angular.mock.$Browser.prototype = { * run all fns in pollFns */ poll: function poll() { - angular.forEach(this.pollFns, function(pollFn){ + angular.forEach(this.pollFns, function(pollFn) { pollFn(); }); }, @@ -149,15 +152,24 @@ angular.mock.$Browser.prototype = { return pollFn; }, - url: function(url, replace) { + url: function(url, replace, state) { + if (angular.isUndefined(state)) { + state = null; + } if (url) { this.$$url = url; + // Native pushState serializes & copies the object; simulate it. + this.$$state = angular.copy(state); return this; } return this.$$url; }, + state: function() { + return this.$$state; + }, + cookies: function(name, value) { if (name) { if (angular.isUndefined(value)) { @@ -189,7 +201,7 @@ angular.mock.$Browser.prototype = { * * @description * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors - * passed into the `$exceptionHandler`. + * passed to the `$exceptionHandler`. */ /** @@ -198,7 +210,7 @@ angular.mock.$Browser.prototype = { * * @description * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed - * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration * information. * * @@ -238,16 +250,15 @@ angular.mock.$ExceptionHandlerProvider = function() { * * @param {string} mode Mode of operation, defaults to `rethrow`. * - * - `rethrow`: If any errors are passed into the handler in tests, it typically - * means that there is a bug in the application or test, so this mock will - * make these tests fail. + * - `rethrow`: If any errors are passed to the handler in tests, it typically means that there + * is a bug in the application or test, so this mock will make these tests fail. * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` * mode stores an array of errors in `$exceptionHandler.errors`, to allow later * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and * {@link ngMock.$log#reset reset()} */ this.mode = function(mode) { - switch(mode) { + switch (mode) { case 'rethrow': handler = function(e) { throw e; @@ -305,7 +316,7 @@ angular.mock.$LogProvider = function() { } }; - this.$get = function () { + this.$get = function() { var $log = { log: function() { $log.log.logs.push(concat([], arguments, 0)); }, warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, @@ -325,13 +336,13 @@ angular.mock.$LogProvider = function() { * @description * Reset all of the logging arrays to empty. */ - $log.reset = function () { + $log.reset = function() { /** * @ngdoc property * @name $log#log.logs * * @description - * Array of messages logged using {@link ngMock.$log#log}. + * Array of messages logged using {@link ng.$log#log `log()`}. * * @example * ```js @@ -345,7 +356,7 @@ angular.mock.$LogProvider = function() { * @name $log#info.logs * * @description - * Array of messages logged using {@link ngMock.$log#info}. + * Array of messages logged using {@link ng.$log#info `info()`}. * * @example * ```js @@ -359,7 +370,7 @@ angular.mock.$LogProvider = function() { * @name $log#warn.logs * * @description - * Array of messages logged using {@link ngMock.$log#warn}. + * Array of messages logged using {@link ng.$log#warn `warn()`}. * * @example * ```js @@ -373,7 +384,7 @@ angular.mock.$LogProvider = function() { * @name $log#error.logs * * @description - * Array of messages logged using {@link ngMock.$log#error}. + * Array of messages logged using {@link ng.$log#error `error()`}. * * @example * ```js @@ -387,7 +398,7 @@ angular.mock.$LogProvider = function() { * @name $log#debug.logs * * @description - * Array of messages logged using {@link ngMock.$log#debug}. + * Array of messages logged using {@link ng.$log#debug `debug()`}. * * @example * ```js @@ -403,21 +414,21 @@ angular.mock.$LogProvider = function() { * @name $log#assertEmpty * * @description - * Assert that the all of the logging methods have no logged messages. If messages present, an - * exception is thrown. + * Assert that all of the logging methods have no logged messages. If any messages are present, + * an exception is thrown. */ $log.assertEmpty = function() { var errors = []; angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { angular.forEach($log[logLevel].logs, function(log) { - angular.forEach(log, function (logItem) { + angular.forEach(log, function(logItem) { errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + (logItem.stack || '')); }); }); }); if (errors.length) { - errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ + errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " + "an expected log message was not checked and removed:"); errors.push(''); throw new Error(errors.join('\n---------\n')); @@ -450,19 +461,19 @@ angular.mock.$LogProvider = function() { * @returns {promise} A promise which will be notified on each iteration. */ angular.mock.$IntervalProvider = function() { - this.$get = ['$rootScope', '$q', - function($rootScope, $q) { + this.$get = ['$browser', '$rootScope', '$q', '$$q', + function($browser, $rootScope, $q, $$q) { var repeatFns = [], nextRepeatId = 0, now = 0; var $interval = function(fn, delay, count, invokeApply) { - var deferred = $q.defer(), - promise = deferred.promise, - iteration = 0, - skipApply = (angular.isDefined(invokeApply) && !invokeApply); + var iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; - count = (angular.isDefined(count)) ? count : 0, + count = (angular.isDefined(count)) ? count : 0; promise.then(null, null, fn); promise.$$intervalId = nextRepeatId; @@ -483,7 +494,11 @@ angular.mock.$IntervalProvider = function() { } } - if (!skipApply) $rootScope.$apply(); + if (skipApply) { + $browser.defer.flush(); + } else { + $rootScope.$apply(); + } } repeatFns.push({ @@ -493,7 +508,7 @@ angular.mock.$IntervalProvider = function() { id: nextRepeatId, deferred: deferred }); - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); nextRepeatId++; return promise; @@ -509,7 +524,7 @@ angular.mock.$IntervalProvider = function() { * @returns {boolean} Returns `true` if the task was successfully cancelled. */ $interval.cancel = function(promise) { - if(!promise) return false; + if (!promise) return false; var fnIndex; angular.forEach(repeatFns, function(fn, index) { @@ -542,7 +557,7 @@ angular.mock.$IntervalProvider = function() { var task = repeatFns[0]; task.fn(); task.nextTime += task.delay; - repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;}); } return millis; }; @@ -570,10 +585,10 @@ function jsonStringToDate(string) { tzMin = int(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.setUTCHours(int(match[4] || 0) - tzHour, + int(match[5] || 0) - tzMin, + int(match[6] || 0), + int(match[7] || 0)); return date; } return string; @@ -590,7 +605,7 @@ function padNumber(num, digits, trim) { num = -num; } num = '' + num; - while(num.length < digits) num = '0' + num; + while (num.length < digits) num = '0' + num; if (trim) num = num.substr(num.length - digits); return neg + num; @@ -634,7 +649,7 @@ function padNumber(num, digits, trim) { * ``` * */ -angular.mock.TzDate = function (offset, timestamp) { +angular.mock.TzDate = function(offset, timestamp) { var self = new Date(0); if (angular.isString(timestamp)) { var tsStr = timestamp; @@ -652,7 +667,7 @@ angular.mock.TzDate = function (offset, timestamp) { } var localOffset = new Date(timestamp).getTimezoneOffset(); - self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; + self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60; self.date = new Date(timestamp + self.offsetDiff); self.getTime = function() { @@ -774,14 +789,23 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) }; }); - $provide.decorator('$animate', function($delegate, $$asyncCallback) { + $provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', + function($delegate, $$asyncCallback, $timeout, $browser) { var animate = { - queue : [], - enabled : $delegate.enabled, - triggerCallbacks : function() { + queue: [], + cancel: $delegate.cancel, + enabled: $delegate.enabled, + triggerCallbackEvents: function() { $$asyncCallback.flush(); }, - triggerReflow : function() { + triggerCallbackPromise: function() { + $timeout.flush(0); + }, + triggerCallbacks: function() { + this.triggerCallbackEvents(); + this.triggerCallbackPromise(); + }, + triggerReflow: function() { angular.forEach(reflowQueue, function(fn) { fn(); }); @@ -790,19 +814,20 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) }; angular.forEach( - ['enter','leave','move','addClass','removeClass','setClass'], function(method) { + ['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) { animate[method] = function() { animate.queue.push({ - event : method, - element : arguments[0], - args : arguments + event: method, + element: arguments[0], + options: arguments[arguments.length - 1], + args: arguments }); - $delegate[method].apply($delegate, arguments); + return $delegate[method].apply($delegate, arguments); }; }); return animate; - }); + }]); }]); @@ -862,13 +887,13 @@ angular.mock.dump = function(object) { function serializeScope(scope, offset) { offset = offset || ' '; var log = [offset + 'Scope(' + scope.$id + '): {']; - for ( var key in scope ) { + for (var key in scope) { if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { log.push(' ' + key + ': ' + angular.toJson(scope[key])); } } var child = scope.$$childHead; - while(child) { + while (child) { log.push(serializeScope(child, offset + ' ')); child = child.$$nextSibling; } @@ -900,7 +925,7 @@ angular.mock.dump = function(object) { * When an Angular application needs some data from a server, it calls the $http service, which * sends the request to a real server using $httpBackend service. With dependency injection, it is * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify - * the requests and respond with some testing data without sending a request to real server. + * the requests and respond with some testing data without sending a request to a real server. * * There are two ways to specify what test data should be returned as http responses by the mock * backend when the code under test makes http requests: @@ -980,6 +1005,11 @@ angular.mock.dump = function(object) { * First we create the controller under test: * ```js + // The module code + angular + .module('MyApp', []) + .controller('MyController', MyController); + // The controller code function MyController($scope, $http) { var authToken; @@ -1007,13 +1037,17 @@ angular.mock.dump = function(object) { ```js // testing controller describe('MyController', function() { - var $httpBackend, $rootScope, createController; + var $httpBackend, $rootScope, createController, authRequestHandler; + + // Set up the module + beforeEach(module('MyApp')); beforeEach(inject(function($injector) { // Set up the mock http service responses $httpBackend = $injector.get('$httpBackend'); // backend definition common for all tests - $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'}); + authRequestHandler = $httpBackend.when('GET', '/auth.py') + .respond({userId: 'userX'}, {'A-Token': 'xxx'}); // Get hold of a scope (i.e. the root scope) $rootScope = $injector.get('$rootScope'); @@ -1039,6 +1073,18 @@ angular.mock.dump = function(object) { }); + it('should fail authentication', function() { + + // Notice how you can change the response even after it was set + authRequestHandler.respond(401, ''); + + $httpBackend.expectGET('/auth.py'); + var controller = createController(); + $httpBackend.flush(); + expect($rootScope.status).toBe('Failed...'); + }); + + it('should send msg to server', function() { var controller = createController(); $httpBackend.flush(); @@ -1073,7 +1119,7 @@ angular.mock.dump = function(object) { ``` */ angular.mock.$HttpBackendProvider = function() { - this.$get = ['$rootScope', createHttpBackendMock]; + this.$get = ['$rootScope', '$timeout', createHttpBackendMock]; }; /** @@ -1090,7 +1136,7 @@ angular.mock.$HttpBackendProvider = function() { * @param {Object=} $browser Auto-flushing enabled if specified * @return {Object} Instance of $httpBackend mock */ -function createHttpBackendMock($rootScope, $delegate, $browser) { +function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) { var definitions = [], expectations = [], responses = [], @@ -1103,7 +1149,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { return function() { return angular.isNumber(status) ? [status, data, headers, statusText] - : [200, status, data]; + : [200, status, data, headers]; }; } @@ -1120,7 +1166,9 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { } function wrapResponse(wrapped) { - if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); + if (!$browser && timeout) { + timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout); + } return handleResponse; @@ -1186,32 +1234,39 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @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))=} 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 * object and returns true if the headers match the current definition. * @returns {requestHandler} Returns an object with `respond` method that controls how a matched - * request is handled. + * 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. * * - respond – * `{function([status,] data[, headers, statusText]) * | function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). + * headers (Object), and the text for the status (string). The respond method returns the + * `requestHandler` object for possible overrides. */ $httpBackend.when = function(method, url, data, headers) { var definition = new MockHttpExpectation(method, url, data, headers), chain = { respond: function(status, data, headers, statusText) { + definition.passThrough = undefined; definition.response = createResponse(status, data, headers, statusText); + return chain; } }; if ($browser) { chain.passThrough = function() { + definition.response = undefined; definition.passThrough = true; + return chain; }; } @@ -1225,10 +1280,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for GET requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1237,10 +1294,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for HEAD requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1249,10 +1308,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for DELETE requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1261,12 +1322,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for POST requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @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))=} 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. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1275,12 +1338,14 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for PUT requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @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))=} 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. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1289,9 +1354,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new backend definition for JSONP requests. For more info see `when()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ createShortMethods('when'); @@ -1303,30 +1370,36 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * Creates a new request expectation. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @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)|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. * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header * object and returns true if the headers match the current expectation. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. * * - respond – * `{function([status,] data[, headers, statusText]) * | function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can * return an array containing response status (number), response data (string), response - * headers (Object), and the text for the status (string). + * headers (Object), and the text for the status (string). The respond method returns the + * `requestHandler` object for possible overrides. */ $httpBackend.expect = function(method, url, data, headers) { - var expectation = new MockHttpExpectation(method, url, data, headers); + var expectation = new MockHttpExpectation(method, url, data, headers), + chain = { + respond: function(status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); + return chain; + } + }; + expectations.push(expectation); - return { - respond: function (status, data, headers, statusText) { - expectation.response = createResponse(status, data, headers, statusText); - } - }; + return chain; }; @@ -1336,10 +1409,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for GET requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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 {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. See #expect for more info. + * 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. See #expect for more info. */ /** @@ -1348,10 +1423,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for HEAD requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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 {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1360,10 +1437,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for DELETE requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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 {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1372,13 +1451,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for POST requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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)|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. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1387,13 +1468,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PUT requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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)|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. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1402,13 +1485,15 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for PATCH requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @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)|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. * @param {Object=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ /** @@ -1417,9 +1502,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * @description * Creates a new request expectation for JSONP requests. For more info see `expect()`. * - * @param {string|RegExp} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` method that control how a matched - * request is handled. + * 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. */ createShortMethods('expect'); @@ -1434,11 +1521,11 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * all pending requests will be flushed. If there are no pending requests when the flush method * is called an exception is thrown (as this typically a sign of programming error). */ - $httpBackend.flush = function(count) { - $rootScope.$digest(); + $httpBackend.flush = function(count, digest) { + if (digest !== false) $rootScope.$digest(); if (!responses.length) throw new Error('No pending request to flush !'); - if (angular.isDefined(count)) { + if (angular.isDefined(count) && count !== null) { while (count--) { if (!responses.length) throw new Error('No more pending request to flush !'); responses.shift()(); @@ -1448,7 +1535,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { responses.shift()(); } } - $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingExpectation(digest); }; @@ -1466,8 +1553,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * afterEach($httpBackend.verifyNoOutstandingExpectation); * ``` */ - $httpBackend.verifyNoOutstandingExpectation = function() { - $rootScope.$digest(); + $httpBackend.verifyNoOutstandingExpectation = function(digest) { + if (digest !== false) $rootScope.$digest(); if (expectations.length) { throw new Error('Unsatisfied requests: ' + expectations.join(', ')); } @@ -1511,7 +1598,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function createShortMethods(prefix) { - angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { + angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { $httpBackend[prefix + method] = function(url, headers) { return $httpBackend[prefix](method, url, undefined, headers); }; @@ -1541,6 +1628,7 @@ function MockHttpExpectation(method, url, data, headers) { this.matchUrl = function(u) { if (!url) return true; if (angular.isFunction(url.test)) return url.test(u); + if (angular.isFunction(url)) return url(u); return url == u; }; @@ -1554,7 +1642,9 @@ function MockHttpExpectation(method, url, data, headers) { if (angular.isUndefined(data)) return true; if (data && angular.isFunction(data.test)) return data.test(d); if (data && angular.isFunction(data)) return data(d); - if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); + if (data && !angular.isString(data)) { + return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); + } return data == d; }; @@ -1627,7 +1717,7 @@ function MockXhr() { * that adds a "flush" and "verifyNoPendingTasks" methods. */ -angular.mock.$TimeoutDecorator = function($delegate, $browser) { +angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $browser) { /** * @ngdoc method @@ -1666,9 +1756,9 @@ angular.mock.$TimeoutDecorator = function($delegate, $browser) { } return $delegate; -}; +}]; -angular.mock.$RAFDecorator = function($delegate) { +angular.mock.$RAFDecorator = ['$delegate', function($delegate) { var queue = []; var rafFn = function(fn) { var index = queue.length; @@ -1681,12 +1771,12 @@ angular.mock.$RAFDecorator = function($delegate) { rafFn.supported = $delegate.supported; rafFn.flush = function() { - if(queue.length === 0) { + if (queue.length === 0) { throw new Error('No rAF callbacks present'); } var length = queue.length; - for(var i=0;i<length;i++) { + for (var i = 0; i < length; i++) { queue[i](); } @@ -1694,9 +1784,9 @@ angular.mock.$RAFDecorator = function($delegate) { }; return rafFn; -}; +}]; -angular.mock.$AsyncCallbackDecorator = function($delegate) { +angular.mock.$AsyncCallbackDecorator = ['$delegate', function($delegate) { var callbacks = []; var addFn = function(fn) { callbacks.push(fn); @@ -1708,7 +1798,7 @@ angular.mock.$AsyncCallbackDecorator = function($delegate) { callbacks = []; }; return addFn; -}; +}]; /** * @@ -1722,11 +1812,12 @@ angular.mock.$RootElementProvider = function() { /** * @ngdoc module * @name ngMock + * @packageName angular-mocks * @description * * # ngMock * - * The `ngMock` module providers support to inject and mock Angular services into unit tests. + * The `ngMock` module provides support to inject and mock Angular services into unit tests. * In addition, ngMock also extends various core ng services such that they can be * inspected and controlled in a synchronous manner within test code. * @@ -1745,12 +1836,14 @@ angular.module('ngMock', ['ng']).provider({ $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); $provide.decorator('$$rAF', angular.mock.$RAFDecorator); $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); + $provide.decorator('$rootScope', angular.mock.$RootScopeDecorator); }]); /** * @ngdoc module * @name ngMockE2E * @module ngMockE2E + * @packageName angular-mocks * @description * * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. @@ -1784,7 +1877,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * use the `passThrough` request handler of `when` instead of `respond`. * * Additionally, we don't want to manually have to flush mocked out requests like we do during unit - * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests + * testing. For this reason the e2e $httpBackend flushes mocked out requests * automatically, closely simulating the behavior of the XMLHttpRequest object. * * To setup the application to run with this http backend, you have to create a module that depends @@ -1800,7 +1893,9 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * * // adds a new phone to the phones array * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { - * phones.push(angular.fromJson(data)); + * var phone = angular.fromJson(data); + * phones.push(phone); + * return [200, phone, {}]; * }); * $httpBackend.whenGET(/^\/templates\//).passThrough(); * //... @@ -1818,12 +1913,14 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * Creates a new backend definition. * * @param {string} method HTTP method. - * @param {string|RegExp} url HTTP url. + * @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)=} 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. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * 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. * * - respond – * `{function([status,] data[, headers, statusText]) @@ -1834,6 +1931,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * - passThrough – `{function()}` – Any request matching a backend definition with * `passThrough` handler will be passed through to the real backend (an XHR request will be made * to the server.) + * - Both methods return the `requestHandler` object for possible overrides. */ /** @@ -1843,10 +1941,12 @@ 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} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * 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. */ /** @@ -1856,10 +1956,12 @@ 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} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * 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. */ /** @@ -1869,10 +1971,12 @@ 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} url HTTP url. + * @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 {(Object|function(Object))=} headers HTTP headers. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * 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. */ /** @@ -1882,11 +1986,13 @@ 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} url HTTP url. + * @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)=} data HTTP request body. * @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. + * 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. */ /** @@ -1896,11 +2002,13 @@ 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} url HTTP url. + * @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)=} data HTTP request body. * @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. + * 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. */ /** @@ -1910,11 +2018,13 @@ 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} url HTTP url. + * @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)=} data HTTP request body. * @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. + * 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. */ /** @@ -1924,31 +2034,104 @@ 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} url HTTP url. + * @param {string|RegExp|function(string)} url HTTP url or function that receives the url + * and returns true if the url match the current definition. * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that - * control how a matched request is handled. + * 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. */ angular.mock.e2e = {}; angular.mock.e2e.$httpBackendDecorator = - ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; + ['$rootScope', '$timeout', '$delegate', '$browser', createHttpBackendMock]; + + +/** + * @ngdoc type + * @name $rootScope.Scope + * @module ngMock + * @description + * {@link ng.$rootScope.Scope Scope} type decorated with helper methods useful for testing. These + * methods are automatically available on any {@link ng.$rootScope.Scope Scope} instance when + * `ngMock` module is loaded. + * + * In addition to all the regular `Scope` methods, the following helper methods are available: + */ +angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) { + var $rootScopePrototype = Object.getPrototypeOf($delegate); -angular.mock.clearDataCache = function() { - var key, - cache = angular.element.cache; + $rootScopePrototype.$countChildScopes = countChildScopes; + $rootScopePrototype.$countWatchers = countWatchers; - for(key in cache) { - if (Object.prototype.hasOwnProperty.call(cache,key)) { - var handle = cache[key].handle; + return $delegate; - handle && angular.element(handle.elem).off(); - delete cache[key]; + // ------------------------------------------------------------------------------------------ // + + /** + * @ngdoc method + * @name $rootScope.Scope#$countChildScopes + * @module ngMock + * @description + * Counts all the direct and indirect child scopes of the current scope. + * + * The current scope is excluded from the count. The count includes all isolate child scopes. + * + * @returns {number} Total number of child scopes. + */ + function countChildScopes() { + // jshint validthis: true + var count = 0; // exclude the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += 1; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } } + + return count; } -}; -if(window.jasmine || window.mocha) { + /** + * @ngdoc method + * @name $rootScope.Scope#$countWatchers + * @module ngMock + * @description + * Counts all the watchers of direct and indirect child scopes of the current scope. + * + * The watchers of the current scope are included in the count and so are all the watchers of + * isolate child scopes. + * + * @returns {number} Total number of watchers. + */ + function countWatchers() { + // jshint validthis: true + var count = this.$$watchers ? this.$$watchers.length : 0; // include the current scope + var pendingChildHeads = [this.$$childHead]; + var currentScope; + + while (pendingChildHeads.length) { + currentScope = pendingChildHeads.shift(); + + while (currentScope) { + count += currentScope.$$watchers ? currentScope.$$watchers.length : 0; + pendingChildHeads.push(currentScope.$$childHead); + currentScope = currentScope.$$nextSibling; + } + } + + return count; + } +}]; + + +if (window.jasmine || window.mocha) { var currentSpec = null, isSpecRunning = function() { @@ -1956,13 +2139,19 @@ if(window.jasmine || window.mocha) { }; - beforeEach(function() { + (window.beforeEach || window.setup)(function() { currentSpec = this; }); - afterEach(function() { + (window.afterEach || window.teardown)(function() { var injector = currentSpec.$injector; + angular.forEach(currentSpec.$modules, function(module) { + if (module && module.$$hashKey) { + module.$$hashKey = undefined; + } + }); + currentSpec.$injector = null; currentSpec.$modules = null; currentSpec = null; @@ -1972,8 +2161,6 @@ if(window.jasmine || window.mocha) { injector.get('$browser').pollFns.length = 0; } - angular.mock.clearDataCache(); - // clean up jquery's fragment cache angular.forEach(angular.element.fragments, function(val, key) { delete angular.element.fragments[key]; @@ -1993,6 +2180,7 @@ if(window.jasmine || window.mocha) { * @description * * *NOTE*: This function is also published on window for easy access.<br> + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha * * This function registers a module configuration code. It collects the configuration information * which will be used when the injector is created by {@link angular.mock.inject inject}. @@ -2002,7 +2190,7 @@ 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 register as values in the module, the key being + * 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. */ window.module = angular.mock.module = function() { @@ -2035,6 +2223,7 @@ if(window.jasmine || window.mocha) { * @description * * *NOTE*: This function is also published on window for easy access.<br> + * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha * * The inject function wraps a function into an injectable function. The inject() creates new * instance of {@link auto.$injector $injector} per test, which is then used for @@ -2134,14 +2323,28 @@ if(window.jasmine || window.mocha) { ///////////////////// function workFn() { var modules = currentSpec.$modules || []; - + var strictDi = !!currentSpec.$injectorStrict; modules.unshift('ngMock'); modules.unshift('ng'); var injector = currentSpec.$injector; if (!injector) { - injector = currentSpec.$injector = angular.injector(modules); + if (strictDi) { + // If strictDi is enabled, annotate the providerInjector blocks + angular.forEach(modules, function(moduleFn) { + if (typeof moduleFn === "function") { + angular.injector.$$annotate(moduleFn); + } + }); + } + injector = currentSpec.$injector = angular.injector(modules, strictDi); + currentSpec.$injectorStrict = strictDi; } - for(var i = 0, ii = blockFns.length; i < ii; i++) { + for (var i = 0, ii = blockFns.length; i < ii; i++) { + if (currentSpec.$injectorStrict) { + // If the injector is strict / strictDi, and the spec wants to inject using automatic + // annotation, then annotate the function here. + injector.annotate(blockFns[i]); + } try { /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ injector.invoke(blockFns[i] || angular.noop, this); @@ -2157,6 +2360,22 @@ if(window.jasmine || window.mocha) { } } }; + + + angular.mock.inject.strictDi = function(value) { + value = arguments.length ? !!value : true; + return isSpecRunning() ? workFn() : workFn; + + function workFn() { + if (value !== currentSpec.$injectorStrict) { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not modify strict annotations'); + } else { + currentSpec.$injectorStrict = value; + } + } + } + }; } diff --git a/xstatic/pkg/angular/data/angular-resource.js b/xstatic/pkg/angular/data/angular-resource.js index 7014984..7a5eac2 100644 --- a/xstatic/pkg/angular/data/angular-resource.js +++ b/xstatic/pkg/angular/data/angular-resource.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -35,7 +35,7 @@ function lookupDottedPath(obj, path) { function shallowClearAndCopy(src, dst) { dst = dst || {}; - angular.forEach(dst, function(value, key){ + angular.forEach(dst, function(value, key) { delete dst[key]; }); @@ -78,6 +78,18 @@ function shallowClearAndCopy(src, dst) { * * Requires the {@link ngResource `ngResource`} module to be installed. * + * By default, trailing slashes will be stripped from the calculated URLs, + * which can pose problems with server backends that do not expect that + * behavior. This can be disabled by configuring the `$resourceProvider` like + * this: + * + * ```js + app.config(['$resourceProvider', function($resourceProvider) { + // Don't strip trailing slashes from calculated URLs + $resourceProvider.defaults.stripTrailingSlashes = false; + }]); + * ``` + * * @param {string} url A parametrized URL template with parameters prefixed by `:` as in * `/user/:username`. If you are using a URL with a port number (e.g. * `http://example.com:8080/api`), it will be respected. @@ -99,12 +111,14 @@ function shallowClearAndCopy(src, dst) { * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in * URL `/path/greet?salutation=Hello`. * - * If the parameter value is prefixed with `@` then the value of that parameter is extracted from - * the data object (useful for non-GET operations). + * If the parameter value is prefixed with `@` then the value for that parameter will be extracted + * from the corresponding property on the `data` object (provided when calling an action method). For + * example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of `someParam` + * will be `data.someProp`. * - * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend + * @param {Object.<Object>=} actions Hash with declaration of custom actions that should extend * the default set of resource actions. The declaration should be created in the format of {@link - * ng.$http#usage_parameters $http.config}: + * ng.$http#usage $http.config}: * * {action1: {method:?, params:?, isArray:?, headers:?, ...}, * action2: {method:?, params:?, isArray:?, headers:?, ...}, @@ -114,8 +128,8 @@ function shallowClearAndCopy(src, dst) { * * - **`action`** – {string} – The name of action. This name becomes the name of the method on * your resource object. - * - **`method`** – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, - * `DELETE`, and `JSONP`. + * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, + * `DELETE`, `JSONP`, etc). * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of * the parameter value is a function, it will be executed every time when a param value needs to * be obtained for a request (unless the param was overridden). @@ -127,10 +141,16 @@ function shallowClearAndCopy(src, dst) { * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. + * By default, transformRequest will contain one function that checks if the request data is + * an object and serializes to using `angular.toJson`. To prevent this behavior, set + * `transformRequest` to an empty array: `transformRequest: []` * - **`transformResponse`** – * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – * transform function or an array of such functions. The transform function takes the http * response body and headers and returns its transformed (typically deserialized) version. + * By default, transformResponse will contain one function that checks if the response looks like + * a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, set + * `transformResponse` to an empty array: `transformResponse: []` * - **`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 @@ -147,6 +167,14 @@ function shallowClearAndCopy(src, dst) { * `response` and `responseError`. Both `response` and `responseError` interceptors get called * with `http response` object. See {@link ng.$http $http interceptors}. * + * @param {Object} options Hash with custom settings that should extend the + * default `$resourceProvider` behavior. The only supported option is + * + * Where: + * + * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing + * slashes from any calculated URL will be stripped. (Defaults to true.) + * * @returns {Object} A resource "class" object with methods for the default set of resource actions * optionally extended with custom `actions`. The default set contains these actions: * ```js @@ -295,20 +323,20 @@ function shallowClearAndCopy(src, dst) { * # Creating a custom 'PUT' request * In this example we create a custom method on our resource to make a PUT request * ```js - * var app = angular.module('app', ['ngResource', 'ngRoute']); + * var app = angular.module('app', ['ngResource', 'ngRoute']); * - * // Some APIs expect a PUT request in the format URL/object/ID - * // Here we are creating an 'update' method - * app.factory('Notes', ['$resource', function($resource) { + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { * return $resource('/notes/:id', null, * { * 'update': { method:'PUT' } * }); - * }]); + * }]); * - * // In our controller we get the ID from the URL using ngRoute and $routeParams - * // We pass in $routeParams and our Notes factory along with $scope - * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', function($scope, $routeParams, Notes) { * // First get a note object from the factory * var note = Notes.get({ id:$routeParams.id }); @@ -318,293 +346,322 @@ function shallowClearAndCopy(src, dst) { * Notes.update({ id:$id }, note); * * // This will PUT /notes/ID with the note object in the request payload - * }]); + * }]); * ``` */ angular.module('ngResource', ['ng']). - factory('$resource', ['$http', '$q', function($http, $q) { - - var DEFAULT_ACTIONS = { - 'get': {method:'GET'}, - 'save': {method:'POST'}, - 'query': {method:'GET', isArray:true}, - 'remove': {method:'DELETE'}, - 'delete': {method:'DELETE'} + provider('$resource', function() { + var provider = this; + + this.defaults = { + // Strip slashes by default + stripTrailingSlashes: true, + + // Default actions configuration + actions: { + 'get': {method: 'GET'}, + 'save': {method: 'POST'}, + 'query': {method: 'GET', isArray: true}, + 'remove': {method: 'DELETE'}, + 'delete': {method: 'DELETE'} + } }; - var noop = angular.noop, + + this.$get = ['$http', '$q', function($http, $q) { + + var noop = angular.noop, forEach = angular.forEach, extend = angular.extend, copy = angular.copy, isFunction = angular.isFunction; - /** - * We need our custom method because encodeURIComponent is too aggressive and doesn't follow - * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path - * segments: - * segment = *pchar - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * pct-encoded = "%" HEXDIG HEXDIG - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriSegment(val) { - return encodeUriQuery(val, true). - replace(/%26/gi, '&'). - replace(/%3D/gi, '='). - replace(/%2B/gi, '+'); - } + /** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set + * (pchar) allowed in path segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } - /** - * This method is intended for encoding *key* or *value* parts of query component. We need a - * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't - * have to be encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) - * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * pct-encoded = "%" HEXDIG HEXDIG - * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - * / "*" / "+" / "," / ";" / "=" - */ - function encodeUriQuery(val, pctEncodeSpaces) { - return encodeURIComponent(val). - replace(/%40/gi, '@'). - replace(/%3A/gi, ':'). - replace(/%24/g, '$'). - replace(/%2C/gi, ','). - replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); - } + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a + * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't + * have to be encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } - function Route(template, defaults) { - this.template = template; - this.defaults = defaults || {}; - this.urlParams = {}; - } + function Route(template, defaults) { + this.template = template; + this.defaults = extend({}, provider.defaults, defaults); + this.urlParams = {}; + } - Route.prototype = { - setUrlParams: function(config, params, actionUrl) { - var self = this, + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, url = actionUrl || self.template, val, encodedVal; - var urlParams = self.urlParams = {}; - forEach(url.split(/\W/), function(param){ - if (param === 'hasOwnProperty') { - throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); - } - if (!(new RegExp("^\\d+$").test(param)) && param && - (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { - urlParams[param] = true; - } - }); - url = url.replace(/\\:/g, ':'); - - params = params || {}; - forEach(self.urlParams, function(_, urlParam){ - val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; - if (angular.isDefined(val) && val !== null) { - encodedVal = encodeUriSegment(val); - url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { - return encodedVal + p1; - }); - } else { - url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, - leadingSlashes, tail) { - if (tail.charAt(0) == '/') { - return tail; - } else { - return leadingSlashes + tail; - } - }); - } - }); + var urlParams = self.urlParams = {}; + forEach(url.split(/\W/), function(param) { + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } + if (!(new RegExp("^\\d+$").test(param)) && param && + (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { + urlParams[param] = true; + } + }); + url = url.replace(/\\:/g, ':'); + + params = params || {}; + forEach(self.urlParams, function(_, urlParam) { + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (angular.isDefined(val) && val !== null) { + encodedVal = encodeUriSegment(val); + url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) == '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); - // strip trailing slashes and set the url - url = url.replace(/\/+$/, '') || '/'; - // then replace collapse `/.` if found in the last URL path segment before the query - // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` - url = url.replace(/\/\.(?=\w+($|\?))/, '.'); - // replace escaped `/\.` with `/.` - config.url = url.replace(/\/\\\./, '/.'); + // strip trailing slashes and set the url (unless this behavior is specifically disabled) + if (self.defaults.stripTrailingSlashes) { + url = url.replace(/\/+$/, '') || '/'; + } + // then replace collapse `/.` if found in the last URL path segment before the query + // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // replace escaped `/\.` with `/.` + config.url = url.replace(/\/\\\./, '/.'); - // set params - delegate param encoding to $http - forEach(params, function(value, key){ - if (!self.urlParams[key]) { - config.params = config.params || {}; - config.params[key] = value; - } - }); - } - }; + // set params - delegate param encoding to $http + forEach(params, function(value, key) { + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; - function resourceFactory(url, paramDefaults, actions) { - var route = new Route(url); - actions = extend({}, DEFAULT_ACTIONS, actions); + function resourceFactory(url, paramDefaults, actions, options) { + var route = new Route(url, options); - function extractParams(data, actionParams){ - var ids = {}; - actionParams = extend({}, paramDefaults, actionParams); - forEach(actionParams, function(value, key){ - if (isFunction(value)) { value = value(); } - ids[key] = value && value.charAt && value.charAt(0) == '@' ? - lookupDottedPath(data, value.substr(1)) : value; - }); - return ids; - } + actions = extend({}, provider.defaults.actions, actions); - function defaultResponseInterceptor(response) { - return response.resource; - } + function extractParams(data, actionParams) { + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key) { + if (isFunction(value)) { value = value(); } + ids[key] = value && value.charAt && value.charAt(0) == '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value) { + shallowClearAndCopy(value || {}, this); + } + + Resource.prototype.toJSON = function() { + var data = extend({}, this); + delete data.$promise; + delete data.$resolved; + return data; + }; - function Resource(value){ - shallowClearAndCopy(value || {}, this); - } + forEach(actions, function(action, name) { + var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); - forEach(actions, function(action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); - - Resource[name] = function(a1, a2, a3, a4) { - var params = {}, data, success, error; - - /* jshint -W086 */ /* (purposefully fall through case statements) */ - switch(arguments.length) { - case 4: - error = a4; - success = a3; - //fallthrough - case 3: - case 2: - if (isFunction(a2)) { - if (isFunction(a1)) { - success = a1; - error = a2; - break; - } + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; - success = a2; - error = a3; + /* jshint -W086 */ /* (purposefully fall through case statements) */ + switch (arguments.length) { + case 4: + error = a4; + success = a3; //fallthrough - } else { - params = a1; - data = a2; - success = a3; - break; - } - case 1: - if (isFunction(a1)) success = a1; - else if (hasBody) data = a1; - else params = a1; - break; - case 0: break; - default: - throw $resourceMinErr('badargs', - "Expected up to 4 arguments [params, data, success, error], got {0} arguments", - arguments.length); - } - /* jshint +W086 */ /* (purposefully fall through case statements) */ - - var isInstanceCall = this instanceof Resource; - var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); - var httpConfig = {}; - var responseInterceptor = action.interceptor && action.interceptor.response || - defaultResponseInterceptor; - var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; - - forEach(action, function(value, key) { - if (key != 'params' && key != 'isArray' && key != 'interceptor') { - httpConfig[key] = copy(value); + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + "Expected up to 4 arguments [params, data, success, error], got {0} arguments", + arguments.length); } - }); + /* jshint +W086 */ /* (purposefully fall through case statements) */ + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + + forEach(action, function(value, key) { + if (key != 'params' && key != 'isArray' && key != 'interceptor') { + httpConfig[key] = copy(value); + } + }); - if (hasBody) httpConfig.data = data; - route.setUrlParams(httpConfig, - extend({}, extractParams(data, action.params || {}), params), - action.url); + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); - var promise = $http(httpConfig).then(function(response) { - var data = response.data, + var promise = $http(httpConfig).then(function(response) { + var data = response.data, promise = value.$promise; - if (data) { - // Need to convert action.isArray to boolean in case it is undefined - // jshint -W018 - if (angular.isArray(data) !== (!!action.isArray)) { - throw $resourceMinErr('badcfg', 'Error in resource configuration. Expected ' + - 'response to contain an {0} but got an {1}', - action.isArray?'array':'object', angular.isArray(data)?'array':'object'); + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + // jshint -W018 + if (angular.isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', + 'Error in resource configuration for action `{0}`. Expected response to ' + + 'contain an {1} but got an {2}', name, action.isArray ? 'array' : 'object', + angular.isArray(data) ? 'array' : 'object'); + } + // jshint +W018 + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + if (typeof item === "object") { + value.push(new Resource(item)); + } else { + // Valid JSON values may be string literals, and these should not be converted + // into objects. These items will not have access to the Resource prototype + // methods, but unfortunately there + value.push(item); + } + }); + } else { + shallowClearAndCopy(data, value); + value.$promise = promise; + } } - // jshint +W018 - if (action.isArray) { - value.length = 0; - forEach(data, function(item) { - value.push(new Resource(item)); - }); - } else { - shallowClearAndCopy(data, value); - value.$promise = promise; - } - } - value.$resolved = true; + value.$resolved = true; - response.resource = value; + response.resource = value; - return response; - }, function(response) { - value.$resolved = true; + return response; + }, function(response) { + value.$resolved = true; - (error||noop)(response); + (error || noop)(response); - return $q.reject(response); - }); + return $q.reject(response); + }); - promise = promise.then( + promise = promise.then( function(response) { var value = responseInterceptor(response); - (success||noop)(value, response.headers); + (success || noop)(value, response.headers); return value; }, responseErrorInterceptor); - if (!isInstanceCall) { - // we are creating instance / collection - // - set the initial promise - // - return the instance / collection - value.$promise = promise; - value.$resolved = false; + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; - return value; - } + return value; + } - // instance call - return promise; - }; + // instance call + return promise; + }; - Resource.prototype['$' + name] = function(params, success, error) { - if (isFunction(params)) { - error = success; success = params; params = {}; - } - var result = Resource[name].call(this, params, this, success, error); - return result.$promise || result; - }; - }); + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); - Resource.bind = function(additionalParamDefaults){ - return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; + Resource.bind = function(additionalParamDefaults) { + return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; - return Resource; - } + return Resource; + } - return resourceFactory; - }]); + return resourceFactory; + }]; + }); })(window, window.angular); diff --git a/xstatic/pkg/angular/data/angular-route.js b/xstatic/pkg/angular/data/angular-route.js index f7ebda8..834429b 100644 --- a/xstatic/pkg/angular/data/angular-route.js +++ b/xstatic/pkg/angular/data/angular-route.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -22,12 +22,12 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider); + provider('$route', $RouteProvider), + $routeMinErr = angular.$$minErr('ngRoute'); /** * @ngdoc provider * @name $routeProvider - * @function * * @description * @@ -39,9 +39,9 @@ var ngRouteModule = angular.module('ngRoute', ['ng']). * ## Dependencies * Requires the {@link ngRoute `ngRoute`} module to be installed. */ -function $RouteProvider(){ +function $RouteProvider() { function inherit(parent, extra) { - return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); + return angular.extend(Object.create(parent), extra); } var routes = {}; @@ -146,27 +146,45 @@ function $RouteProvider(){ * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { + //copy original route object to preserve params inherited from proto chain + var routeCopy = angular.copy(route); + if (angular.isUndefined(routeCopy.reloadOnSearch)) { + routeCopy.reloadOnSearch = true; + } + if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { + routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; + } routes[path] = angular.extend( - {reloadOnSearch: true}, - route, - path && pathRegExp(path, route) + routeCopy, + path && pathRegExp(path, routeCopy) ); // create redirection for trailing slashes if (path) { - var redirectPath = (path[path.length-1] == '/') - ? path.substr(0, path.length-1) - : path +'/'; + var redirectPath = (path[path.length - 1] == '/') + ? path.substr(0, path.length - 1) + : path + '/'; routes[redirectPath] = angular.extend( {redirectTo: path}, - pathRegExp(redirectPath, route) + pathRegExp(redirectPath, routeCopy) ); } return this; }; + /** + * @ngdoc property + * @name $routeProvider#caseInsensitiveMatch + * @description + * + * A boolean property indicating if routes defined + * using this provider should be matched using a case insensitive + * algorithm. Defaults to `false`. + */ + this.caseInsensitiveMatch = false; + /** * @param path {string} path * @param opts {Object} options @@ -188,7 +206,7 @@ function $RouteProvider(){ path = path .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ + .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { var optional = option === '?' ? option : null; var star = option === '*' ? option : null; keys.push({ name: key, optional: !!optional }); @@ -216,10 +234,14 @@ function $RouteProvider(){ * Sets route definition that will be used on route change when no other route definition * is matched. * - * @param {Object} params Mapping information to be assigned to `$route.current`. + * @param {Object|string} params Mapping information to be assigned to `$route.current`. + * If called with a string, the value maps to `redirectTo`. * @returns {Object} self */ this.otherwise = function(params) { + if (typeof params === 'string') { + params = {redirectTo: params}; + } this.when(null, params); return this; }; @@ -230,10 +252,9 @@ function $RouteProvider(){ '$routeParams', '$q', '$injector', - '$http', - '$templateCache', + '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { /** * @ngdoc service @@ -270,9 +291,6 @@ function $RouteProvider(){ * This example shows how changing the URL hash causes the `$route` to match a route against the * URL, and the `ngView` pulls in the partial. * - * Note that this example is using {@link ng.directive:script inlined templates} - * to get it working on jsfiddle as well. - * * <example name="$route-service" module="ngRouteExample" * deps="angular-route.js" fixBase="true"> * <file name="index.html"> @@ -380,6 +398,10 @@ function $RouteProvider(){ * defined in `resolve` route property. Once all of the dependencies are resolved * `$routeChangeSuccess` is fired. * + * The route change (and the `$location` change that triggered it) can be prevented + * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} + * for more details about event object. + * * @param {Object} angularEvent Synthetic event object. * @param {Route} next Future route information. * @param {Route} current Current route information. @@ -424,6 +446,8 @@ function $RouteProvider(){ */ var forceReload = false, + preparedRoute, + preparedRouteIsUpdateOnly, $route = { routes: routes, @@ -436,15 +460,50 @@ function $RouteProvider(){ * {@link ng.$location $location} hasn't changed. * * As a result of that, {@link ngRoute.directive:ngView ngView} - * creates new scope, reinstantiates the controller. + * creates new scope and reinstantiates the controller. */ reload: function() { forceReload = true; - $rootScope.$evalAsync(updateRoute); + $rootScope.$evalAsync(function() { + // Don't support cancellation of a reload for now... + prepareRoute(); + commitRoute(); + }); + }, + + /** + * @ngdoc method + * @name $route#updateParams + * + * @description + * Causes `$route` service to update the current URL, replacing + * current route parameters with those specified in `newParams`. + * Provided property names that match the route's path segment + * definitions will be interpolated into the location's path, while + * remaining properties will be treated as query params. + * + * @param {Object} newParams mapping of URL parameter names to values + */ + updateParams: function(newParams) { + if (this.current && this.current.$$route) { + var searchParams = {}, self=this; + + angular.forEach(Object.keys(newParams), function(key) { + if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; + }); + + newParams = angular.extend({}, this.current.params, newParams); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); + $location.search(angular.extend({}, $location.search(), searchParams)); + } + else { + throw $routeMinErr('norout', 'Tried updating route when with no current route'); + } } }; - $rootScope.$on('$locationChangeSuccess', updateRoute); + $rootScope.$on('$locationChangeStart', prepareRoute); + $rootScope.$on('$locationChangeSuccess', commitRoute); return $route; @@ -473,9 +532,7 @@ function $RouteProvider(){ for (var i = 1, len = m.length; i < len; ++i) { var key = keys[i - 1]; - var val = 'string' == typeof m[i] - ? decodeURIComponent(m[i]) - : m[i]; + var val = m[i]; if (key && val) { params[key.name] = val; @@ -484,56 +541,69 @@ function $RouteProvider(){ return params; } - function updateRoute() { - var next = parseRoute(), - last = $route.current; - - if (next && last && next.$$route === last.$$route - && angular.equals(next.pathParams, last.pathParams) - && !next.reloadOnSearch && !forceReload) { - last.params = next.params; - angular.copy(last.params, $routeParams); - $rootScope.$broadcast('$routeUpdate', last); - } else if (next || last) { + function prepareRoute($locationEvent) { + var lastRoute = $route.current; + + preparedRoute = parseRoute(); + preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route + && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) + && !preparedRoute.reloadOnSearch && !forceReload; + + if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { + if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { + if ($locationEvent) { + $locationEvent.preventDefault(); + } + } + } + } + + function commitRoute() { + var lastRoute = $route.current; + var nextRoute = preparedRoute; + + if (preparedRouteIsUpdateOnly) { + lastRoute.params = nextRoute.params; + angular.copy(lastRoute.params, $routeParams); + $rootScope.$broadcast('$routeUpdate', lastRoute); + } else if (nextRoute || lastRoute) { forceReload = false; - $rootScope.$broadcast('$routeChangeStart', next, last); - $route.current = next; - if (next) { - if (next.redirectTo) { - if (angular.isString(next.redirectTo)) { - $location.path(interpolate(next.redirectTo, next.params)).search(next.params) + $route.current = nextRoute; + if (nextRoute) { + if (nextRoute.redirectTo) { + if (angular.isString(nextRoute.redirectTo)) { + $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) .replace(); } else { - $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) + $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) .replace(); } } } - $q.when(next). + $q.when(nextRoute). then(function() { - if (next) { - var locals = angular.extend({}, next.resolve), + if (nextRoute) { + var locals = angular.extend({}, nextRoute.resolve), template, templateUrl; angular.forEach(locals, function(value, key) { locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value); + $injector.get(value) : $injector.invoke(value, null, null, key); }); - if (angular.isDefined(template = next.template)) { + if (angular.isDefined(template = nextRoute.template)) { if (angular.isFunction(template)) { - template = template(next.params); + template = template(nextRoute.params); } - } else if (angular.isDefined(templateUrl = next.templateUrl)) { + } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(next.params); + templateUrl = templateUrl(nextRoute.params); } templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (angular.isDefined(templateUrl)) { - next.loadedTemplateUrl = templateUrl; - template = $http.get(templateUrl, {cache: $templateCache}). - then(function(response) { return response.data; }); + nextRoute.loadedTemplateUrl = templateUrl; + template = $templateRequest(templateUrl); } } if (angular.isDefined(template)) { @@ -544,16 +614,16 @@ function $RouteProvider(){ }). // after route change then(function(locals) { - if (next == $route.current) { - if (next) { - next.locals = locals; - angular.copy(next.params, $routeParams); + if (nextRoute == $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); } - $rootScope.$broadcast('$routeChangeSuccess', next, last); + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); } }, function(error) { - if (next == $route.current) { - $rootScope.$broadcast('$routeChangeError', next, last, error); + if (nextRoute == $route.current) { + $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } }); } @@ -583,11 +653,11 @@ function $RouteProvider(){ */ function interpolate(string, params) { var result = []; - angular.forEach((string||'').split(':'), function(segment, i) { + angular.forEach((string || '').split(':'), function(segment, i) { if (i === 0) { result.push(segment); } else { - var segmentMatch = segment.match(/(\w+)(.*)/); + var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); var key = segmentMatch[1]; result.push(params[key]); result.push(segmentMatch[2] || ''); @@ -632,7 +702,7 @@ ngRouteModule.provider('$routeParams', $RouteParamsProvider); * // Route: /Chapter/:chapterId/Section/:sectionId * // * // Then - * $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} + * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} * ``` */ function $RouteParamsProvider() { @@ -695,7 +765,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); <pre>$location.path() = {{main.$location.path()}}</pre> <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre> <pre>$route.current.params = {{main.$route.current.params}}</pre> - <pre>$route.current.scope.name = {{main.$route.current.scope.name}}</pre> <pre>$routeParams = {{main.$routeParams}}</pre> </div> </file> @@ -719,7 +788,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); .view-animate-container { position:relative; height:100px!important; - position:relative; background:white; border:1px solid black; height:40px; @@ -773,7 +841,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); controllerAs: 'chapter' }); - // configure html5 to get links working on jsfiddle $locationProvider.html5Mode(true); }]) .controller('MainCtrl', ['$route', '$routeParams', '$location', @@ -820,7 +887,7 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Emitted every time the ngView content is reloaded. */ ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; -function ngViewFactory( $route, $anchorScroll, $animate) { +function ngViewFactory($route, $anchorScroll, $animate) { return { restrict: 'ECA', terminal: true, @@ -829,7 +896,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { link: function(scope, $element, attr, ctrl, $transclude) { var currentScope, currentElement, - previousElement, + previousLeaveAnimation, autoScrollExp = attr.autoscroll, onloadExp = attr.onload || ''; @@ -837,19 +904,20 @@ function ngViewFactory( $route, $anchorScroll, $animate) { update(); function cleanupLastView() { - if(previousElement) { - previousElement.remove(); - previousElement = null; + if (previousLeaveAnimation) { + $animate.cancel(previousLeaveAnimation); + previousLeaveAnimation = null; } - if(currentScope) { + + if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement, function() { - previousElement = null; + if (currentElement) { + previousLeaveAnimation = $animate.leave(currentElement); + previousLeaveAnimation.then(function() { + previousLeaveAnimation = null; }); - previousElement = currentElement; currentElement = null; } } @@ -869,7 +937,7 @@ function ngViewFactory( $route, $anchorScroll, $animate) { // function is called before linking the content, which would apply child // directives to non existing elements. var clone = $transclude(newScope, function(clone) { - $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { if (angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); diff --git a/xstatic/pkg/angular/data/angular-sanitize.js b/xstatic/pkg/angular/data/angular-sanitize.js index b670812..4b0edac 100644 --- a/xstatic/pkg/angular/data/angular-sanitize.js +++ b/xstatic/pkg/angular/data/angular-sanitize.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -42,35 +42,36 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); /** * @ngdoc service * @name $sanitize - * @function + * @kind function * * @description - * The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are * then serialized back to properly escaped html string. This means that no unsafe input can make * it into the returned string, however, since our parser is more strict than a typical browser * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. + * browser, won't make it through the sanitizer. The input may also contain SVG markup. * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. * - * @param {string} html Html input. - * @returns {string} Sanitized html. + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. * * @example - <example module="ngSanitize" deps="angular-sanitize.js"> + <example module="sanitizeExample" deps="angular-sanitize.js"> <file name="index.html"> <script> - function Ctrl($scope, $sce) { - $scope.snippet = - '<p style="color:blue">an html\n' + - '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + - 'snippet</p>'; - $scope.deliberatelyTrustDangerousSnippet = function() { - return $sce.trustAsHtml($scope.snippet); - }; - } + angular.module('sanitizeExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) { + $scope.snippet = + '<p style="color:blue">an html\n' + + '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + + 'snippet</p>'; + $scope.deliberatelyTrustDangerousSnippet = function() { + return $sce.trustAsHtml($scope.snippet); + }; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> <table> <tr> @@ -158,14 +159,15 @@ function sanitizeText(chars) { // Regular Expressions for parsing tags and attributes var START_TAG_REGEXP = - /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/, - END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/, + /^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/, + END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/, ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g, BEGIN_TAG_REGEXP = /^</, - BEGING_END_TAGE_REGEXP = /^<\s*\//, + BEGING_END_TAGE_REGEXP = /^<\//, COMMENT_REGEXP = /<!--(.*?)-->/g, DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i, CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g, + SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, // Match everything outside of normal chars and " (quote character) NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; @@ -196,6 +198,12 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); +// 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"); // Special Elements (can contain anything) var specialElements = makeMap("script,style"); @@ -204,16 +212,41 @@ var validElements = angular.extend({}, voidElements, blockElements, inlineElements, - optionalEndTagElements); + optionalEndTagElements, + svgElements); //Attributes that have href and hence need to be sanitized -var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap"); -var validAttrs = angular.extend({}, uriAttrs, 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,'+ - 'valign,value,vspace,width')); +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,' + + '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'); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); function makeMap(str) { var obj = {}, items = str.split(','), i; @@ -234,78 +267,93 @@ function makeMap(str) { * @param {string} html string * @param {object} handler */ -function htmlParser( html, handler ) { - var index, chars, match, stack = [], last = html; +function htmlParser(html, handler) { + if (typeof html !== 'string') { + if (html === null || typeof html === 'undefined') { + html = ''; + } else { + html = '' + html; + } + } + var index, chars, match, stack = [], last = html, text; stack.last = function() { return stack[ stack.length - 1 ]; }; - while ( html ) { + while (html) { + text = ''; chars = true; // Make sure we're not in a script or style element - if ( !stack.last() || !specialElements[ stack.last() ] ) { + if (!stack.last() || !specialElements[ stack.last() ]) { // Comment - if ( html.indexOf("<!--") === 0 ) { + if (html.indexOf("<!--") === 0) { // comments containing -- are not allowed unless they terminate the comment index = html.indexOf("--", 4); - if ( index >= 0 && html.lastIndexOf("-->", index) === index) { - if (handler.comment) handler.comment( html.substring( 4, index ) ); - html = html.substring( index + 3 ); + if (index >= 0 && html.lastIndexOf("-->", index) === index) { + if (handler.comment) handler.comment(html.substring(4, index)); + html = html.substring(index + 3); chars = false; } // DOCTYPE - } else if ( DOCTYPE_REGEXP.test(html) ) { - match = html.match( DOCTYPE_REGEXP ); + } else if (DOCTYPE_REGEXP.test(html)) { + match = html.match(DOCTYPE_REGEXP); - if ( match ) { - html = html.replace( match[0], ''); + if (match) { + html = html.replace(match[0], ''); chars = false; } // end tag - } else if ( BEGING_END_TAGE_REGEXP.test(html) ) { - match = html.match( END_TAG_REGEXP ); + } else if (BEGING_END_TAGE_REGEXP.test(html)) { + match = html.match(END_TAG_REGEXP); - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( END_TAG_REGEXP, parseEndTag ); + if (match) { + html = html.substring(match[0].length); + match[0].replace(END_TAG_REGEXP, parseEndTag); chars = false; } // start tag - } else if ( BEGIN_TAG_REGEXP.test(html) ) { - match = html.match( START_TAG_REGEXP ); - - if ( match ) { - html = html.substring( match[0].length ); - match[0].replace( START_TAG_REGEXP, parseStartTag ); + } else if (BEGIN_TAG_REGEXP.test(html)) { + match = html.match(START_TAG_REGEXP); + + if (match) { + // We only have a valid start-tag if there is a '>'. + if (match[4]) { + html = html.substring(match[0].length); + match[0].replace(START_TAG_REGEXP, parseStartTag); + } chars = false; + } else { + // no ending tag found --- this piece should be encoded as an entity. + text += '<'; + html = html.substring(1); } } - if ( chars ) { + if (chars) { index = html.indexOf("<"); - var text = index < 0 ? html : html.substring( 0, index ); - html = index < 0 ? "" : html.substring( index ); + text += index < 0 ? html : html.substring(0, index); + html = index < 0 ? "" : html.substring(index); - if (handler.chars) handler.chars( decodeEntities(text) ); + if (handler.chars) handler.chars(decodeEntities(text)); } } else { html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), - function(all, text){ + function(all, text) { text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1"); - if (handler.chars) handler.chars( decodeEntities(text) ); + if (handler.chars) handler.chars(decodeEntities(text)); return ""; }); - parseEndTag( "", stack.last() ); + parseEndTag("", stack.last()); } - if ( html == last ) { + if (html == last) { throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " + "of html: {0}", html); } @@ -315,22 +363,22 @@ function htmlParser( html, handler ) { // Clean up any remaining tags parseEndTag(); - function parseStartTag( tag, tagName, rest, unary ) { + function parseStartTag(tag, tagName, rest, unary) { tagName = angular.lowercase(tagName); - if ( blockElements[ tagName ] ) { - while ( stack.last() && inlineElements[ stack.last() ] ) { - parseEndTag( "", stack.last() ); + if (blockElements[ tagName ]) { + while (stack.last() && inlineElements[ stack.last() ]) { + parseEndTag("", stack.last()); } } - if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) { - parseEndTag( "", tagName ); + if (optionalEndTagElements[ tagName ] && stack.last() == tagName) { + parseEndTag("", tagName); } unary = voidElements[ tagName ] || !!unary; - if ( !unary ) - stack.push( tagName ); + if (!unary) + stack.push(tagName); var attrs = {}; @@ -343,22 +391,22 @@ function htmlParser( html, handler ) { attrs[name] = decodeEntities(value); }); - if (handler.start) handler.start( tagName, attrs, unary ); + if (handler.start) handler.start(tagName, attrs, unary); } - function parseEndTag( tag, tagName ) { + 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 ) + for (pos = stack.length - 1; pos >= 0; pos--) + if (stack[ pos ] == tagName) break; - if ( pos >= 0 ) { + if (pos >= 0) { // Close all the open elements, up the stack - for ( i = stack.length - 1; i >= pos; i-- ) - if (handler.end) handler.end( stack[ i ] ); + for (i = stack.length - 1; i >= pos; i--) + if (handler.end) handler.end(stack[ i ]); // Remove the open elements from the stack stack.length = pos; @@ -404,7 +452,12 @@ function decodeEntities(value) { function encodeEntities(value) { return value. replace(/&/g, '&'). - replace(NON_ALPHANUMERIC_REGEXP, function(value){ + replace(SURROGATE_PAIR_REGEXP, function(value) { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). + replace(NON_ALPHANUMERIC_REGEXP, function(value) { return '&#' + value.charCodeAt(0) + ';'; }). replace(/</g, '<'). @@ -421,11 +474,11 @@ function encodeEntities(value) { * comment: function(text) {} * } */ -function htmlSanitizeWriter(buf, uriValidator){ +function htmlSanitizeWriter(buf, uriValidator) { var ignore = false; var out = angular.bind(buf, buf.push); return { - start: function(tag, attrs, unary){ + start: function(tag, attrs, unary) { tag = angular.lowercase(tag); if (!ignore && specialElements[tag]) { ignore = tag; @@ -433,7 +486,7 @@ function htmlSanitizeWriter(buf, uriValidator){ if (!ignore && validElements[tag] === true) { out('<'); out(tag); - angular.forEach(attrs, function(value, key){ + angular.forEach(attrs, function(value, key) { var lkey=angular.lowercase(key); var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); if (validAttrs[lkey] === true && @@ -448,7 +501,7 @@ function htmlSanitizeWriter(buf, uriValidator){ out(unary ? '/>' : '>'); } }, - end: function(tag){ + end: function(tag) { tag = angular.lowercase(tag); if (!ignore && validElements[tag] === true) { out('</'); @@ -459,7 +512,7 @@ function htmlSanitizeWriter(buf, uriValidator){ ignore = false; } }, - chars: function(chars){ + chars: function(chars) { if (!ignore) { out(encodeEntities(chars)); } @@ -476,7 +529,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); /** * @ngdoc filter * @name linky - * @function + * @kind function * * @description * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and @@ -492,20 +545,21 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); <span ng-bind-html="linky_expression | linky"></span> * * @example - <example module="ngSanitize" deps="angular-sanitize.js"> + <example module="linkyExample" deps="angular-sanitize.js"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.snippet = - 'Pretty text with some links:\n'+ - 'http://angularjs.org/,\n'+ - 'mailto:us@somewhere.org,\n'+ - 'another@somewhere.org,\n'+ - 'and one more: ftp://127.0.0.1/.'; - $scope.snippetWithTarget = 'http://angularjs.org/'; - } + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithTarget = 'http://angularjs.org/'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> <table> <tr> @@ -574,7 +628,7 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); */ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { var LINKY_URL_REGEXP = - /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/, MAILTO_REGEXP = /^mailto:/; return function(text, target) { @@ -587,8 +641,10 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; - // if we did not match ftp/http/mailto then assume mailto - if (match[2] == match[3]) url = 'mailto:' + url; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } i = match.index; addText(raw.substr(0, i)); addLink(url, match[0].replace(MAILTO_REGEXP, '')); @@ -607,13 +663,13 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { function addLink(url, text) { html.push('<a '); if (angular.isDefined(target)) { - html.push('target="'); - html.push(target); - html.push('" '); + html.push('target="', + target, + '" '); } - html.push('href="'); - html.push(url); - html.push('">'); + html.push('href="', + url.replace(/"/g, '"'), + '">'); addText(text); html.push('</a>'); } diff --git a/xstatic/pkg/angular/data/angular-scenario.js b/xstatic/pkg/angular/data/angular-scenario.js index 81491c5..d0614ab 100644 --- a/xstatic/pkg/angular/data/angular-scenario.js +++ b/xstatic/pkg/angular/data/angular-scenario.js @@ -1,17 +1,41 @@ /*! - * jQuery JavaScript Library v1.10.2 + * jQuery JavaScript Library v2.1.1 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * - * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-07-03T13:48Z + * Date: 2014-05-01T17:11Z */ -(function( window, undefined ) {'use strict'; + +(function( global, factory ) {'use strict'; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper window is present, + // execute the factory and get jQuery + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a jQuery-making factory as module.exports + // This accentuates the need for the creation of a real window + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { // Can't do this because several apps including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if @@ -19,74 +43,43 @@ // Support: Firefox 18+ // -var - // The deferred used on DOM ready - readyList, +var arr = []; - // A central reference to the root jQuery(document) - rootjQuery, +var slice = arr.slice; - // Support: IE<10 - // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` - core_strundefined = typeof undefined, +var concat = arr.concat; - // Use the correct document accordingly with window argument (sandbox) - location = window.location, - document = window.document, - docElem = document.documentElement, +var push = arr.push; - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, +var indexOf = arr.indexOf; - // Map over the $ in case of overwrite - _$ = window.$, +var class2type = {}; - // [[Class]] -> type pairs - class2type = {}, +var toString = class2type.toString; - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], +var hasOwn = class2type.hasOwnProperty; - core_version = "1.10.2", +var support = {}; - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, + + +var + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + version = "2.1.1", // Define a local copy of jQuery jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); }, - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + // Support: Android<4.1 + // Make sure we trim BOM and NBSP rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - // A simple way to check for HTML strings - // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - // Matches dashed string for camelizing rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, @@ -94,134 +87,13 @@ var // Used by jQuery.camelCase as callback to replace() fcamelCase = function( all, letter ) { return letter.toUpperCase(); - }, - - // The ready event handler - completed = function( event ) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } }; jQuery.fn = jQuery.prototype = { // The current version of jQuery being used - jquery: core_version, + jquery: version, constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, // Start with an empty selector selector: "", @@ -230,19 +102,19 @@ jQuery.fn = jQuery.prototype = { length: 0, toArray: function() { - return core_slice.call( this ); + return slice.call( this ); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array get: function( num ) { - return num == null ? + return num != null ? - // Return a 'clean' array - this.toArray() : + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); + // Return all the elements in a clean array + slice.call( this ); }, // Take an array of elements and push it onto the stack @@ -267,15 +139,14 @@ jQuery.fn = jQuery.prototype = { return jQuery.each( this, callback, args ); }, - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); }, slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); + return this.pushStack( slice.apply( this, arguments ) ); }, first: function() { @@ -292,28 +163,19 @@ jQuery.fn = jQuery.prototype = { return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); }, - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice + push: push, + sort: arr.sort, + splice: arr.splice }; -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, + var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, @@ -322,9 +184,10 @@ jQuery.extend = jQuery.fn.extend = function() { // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; - target = arguments[1] || {}; + // skip the boolean and the target - i = 2; + target = arguments[ i ] || {}; + i++; } // Handle case when target is a string or something (possible in deep copy) @@ -333,9 +196,9 @@ jQuery.extend = jQuery.fn.extend = function() { } // extend jQuery itself if only one argument is passed - if ( length === i ) { + if ( i === length ) { target = this; - --i; + i--; } for ( ; i < length; i++ ) { @@ -378,66 +241,16 @@ jQuery.extend = jQuery.fn.extend = function() { jQuery.extend({ // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), - - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - return jQuery; - }, + // Assume jQuery is ready without the ready module + isReady: true, - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } + error: function( msg ) { + throw new Error( msg ); }, - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, + noop: function() {}, // See test/unit/core.js for details concerning isFunction. // Since version 1.3, DOM methods and functions like alert @@ -446,63 +259,36 @@ jQuery.extend({ return jQuery.type(obj) === "function"; }, - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, + isArray: Array.isArray, isWindow: function( obj ) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; + return obj != null && obj === obj.window; }, isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0; }, isPlainObject: function( obj ) { - var key; - - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { return false; } - try { - // Not own constructor property must be Object - if ( obj.constructor && - !core_hasOwn.call(obj, "constructor") && - !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { return false; } - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if ( jQuery.support.ownLast ) { - for ( key in obj ) { - return core_hasOwn.call( obj, key ); - } - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - for ( key in obj ) {} - - return key === undefined || core_hasOwn.call( obj, key ); + // If the function hasn't returned already, we're confident that + // |obj| is a plain object, created by {} or constructed with new Object + return true; }, isEmptyObject: function( obj ) { @@ -513,105 +299,36 @@ jQuery.extend({ return true; }, - error: function( msg ) { - throw new Error( msg ); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - if ( scripts ) { - jQuery( scripts ).remove(); + type: function( obj ) { + if ( obj == null ) { + return obj + ""; } - return jQuery.merge( [], parsed.childNodes ); + // Support: Android < 4.0, iOS < 6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; }, - parseJSON: function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - } - } - } + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; - jQuery.error( "Invalid JSON: " + data ); - }, + code = jQuery.trim( code ); - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); + if ( code ) { + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf("use strict") === 1 ) { + script = document.createElement("script"); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + indirect( code ); } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); } }, @@ -675,20 +392,12 @@ jQuery.extend({ return obj; }, - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? - function( text ) { - return text == null ? - "" : - core_trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, // results is for internal usage only makeArray: function( arr, results ) { @@ -701,7 +410,7 @@ jQuery.extend({ [ arr ] : arr ); } else { - core_push.call( ret, arr ); + push.call( ret, arr ); } } @@ -709,40 +418,16 @@ jQuery.extend({ }, inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( core_indexOf ) { - return core_indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; + return arr == null ? -1 : indexOf.call( arr, elem, i ); }, merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; + var len = +second.length, + j = 0, + i = first.length; - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; } first.length = i; @@ -750,23 +435,23 @@ jQuery.extend({ return first; }, - grep: function( elems, callback, inv ) { - var retVal, - ret = [], + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], i = 0, - length = elems.length; - inv = !!inv; + length = elems.length, + callbackExpect = !invert; // Go through the array, only saving the items // that pass the validator function for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); } } - return ret; + return matches; }, // arg is for internal usage only @@ -777,13 +462,13 @@ jQuery.extend({ isArray = isArraylike( elems ), ret = []; - // Go through the array, translating each of the items to their + // Go through the array, translating each of the items to their new values if ( isArray ) { for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } @@ -793,13 +478,13 @@ jQuery.extend({ value = callback( elems[ i ], i, arg ); if ( value != null ) { - ret[ ret.length ] = value; + ret.push( value ); } } } // Flatten any nested arrays - return core_concat.apply( [], ret ); + return concat.apply( [], ret ); }, // A global GUID counter for objects @@ -808,7 +493,7 @@ jQuery.extend({ // Bind a function to a context, optionally partially applying any // arguments. proxy: function( fn, context ) { - var args, proxy, tmp; + var tmp, args, proxy; if ( typeof context === "string" ) { tmp = fn[ context ]; @@ -823,9 +508,9 @@ jQuery.extend({ } // Simulated bind - args = core_slice.call( arguments, 2 ); + args = slice.call( arguments, 2 ); proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); }; // Set the guid of unique handler to the same of original handler, so it can be removed @@ -834,148 +519,13 @@ jQuery.extend({ return proxy; }, - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations. - // Note: this method belongs to the css module but it's needed here for the support module. - // If support gets modularized, this method should be moved back to the css module. - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } + now: Date.now, - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support }); -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - // Populate the class2type map jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); @@ -985,7 +535,7 @@ function isArraylike( obj ) { var length = obj.length, type = jQuery.type( obj ); - if ( jQuery.isWindow( obj ) ) { + if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } @@ -993,34 +543,33 @@ function isArraylike( obj ) { return true; } - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; } - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); +var Sizzle = /*! - * Sizzle CSS Selector Engine v1.10.2 + * Sizzle CSS Selector Engine v1.10.19 * http://sizzlejs.com/ * * Copyright 2013 jQuery Foundation, Inc. and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2013-07-03 + * Date: 2014-04-18 */ -(function( window, undefined ) { +(function( window ) { var i, support, - cachedruns, Expr, getText, isXML, + tokenize, compile, + select, outermostContext, sortInput, + hasDuplicate, // Local document vars setDocument, @@ -1040,11 +589,9 @@ var i, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), - hasDuplicate = false, sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; - return 0; } return 0; }, @@ -1086,17 +633,23 @@ var i, // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier identifier = characterEncoding.replace( "w", "w#" ), - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + characterEncoding + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), @@ -1104,8 +657,7 @@ var i, rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - rsibling = new RegExp( whitespace + "*[+~]" ), - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), @@ -1126,14 +678,15 @@ var i, whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + rnative = /^[^{]+\{\s*\[native \w/, // Easily-parseable/retrievable ID or TAG or CLASS selectors rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - + rsibling = /[+~]/, rescape = /'|\\/g, // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters @@ -1141,12 +694,12 @@ var i, funescape = function( _, escaped, escapedWhitespace ) { var high = "0x" + escaped - 0x10000; // NaN means non-codepoint - // Support: Firefox + // Support: Firefox<24 // Workaround erroneous numeric interpretation of +"0x" return high !== high || escapedWhitespace ? escaped : - // BMP codepoint high < 0 ? + // BMP codepoint String.fromCharCode( high + 0x10000 ) : // Supplemental Plane codepoint (surrogate pair) String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); @@ -1210,7 +763,7 @@ function Sizzle( selector, context, results, seed ) { if ( nodeType === 9 ) { elem = context.getElementById( m ); // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 + // nodes that are no longer in the document (jQuery #6963) if ( elem && elem.parentNode ) { // Handle the case where IE, Opera, and Webkit return items // by name instead of ID @@ -1266,7 +819,7 @@ function Sizzle( selector, context, results, seed ) { while ( i-- ) { groups[i] = nid + toSelector( groups[i] ); } - newContext = rsibling.test( selector ) && context.parentNode || context; + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; newSelector = groups.join(","); } @@ -1301,11 +854,11 @@ function createCache() { function cache( key, value ) { // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { + if ( keys.push( key + " " ) > Expr.cacheLength ) { // Only keep the most recent entries delete cache[ keys.shift() ]; } - return (cache[ key ] = value); + return (cache[ key + " " ] = value); } return cache; } @@ -1428,8 +981,21 @@ function createPositionalPseudo( fn ) { } /** - * Detect xml + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== strundefined && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { // documentElement is verified for cases where it doesn't yet exist @@ -1438,16 +1004,14 @@ isXML = Sizzle.isXML = function( elem ) { return documentElement ? documentElement.nodeName !== "HTML" : false; }; -// Expose support vars for convenience -support = Sizzle.support = {}; - /** * Sets document-related variables once based on the current document * @param {Element|Object} [doc] An element or document object to use to set the document * @returns {Object} Returns the current document */ setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc, + var hasCompare, + doc = node ? node.ownerDocument || node : preferredDoc, parent = doc.defaultView; // If no document and documentElement is available, return @@ -1466,10 +1030,17 @@ setDocument = Sizzle.setDocument = function( node ) { // If iframe document is assigned to "document" variable and if iframe has been reloaded, // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent.attachEvent && parent !== parent.top ) { - parent.attachEvent( "onbeforeunload", function() { - setDocument(); - }); + if ( parent && parent !== parent.top ) { + // IE11 does not have attachEvent, so all must suffer + if ( parent.addEventListener ) { + parent.addEventListener( "unload", function() { + setDocument(); + }, false ); + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", function() { + setDocument(); + }); + } } /* Attributes @@ -1492,7 +1063,7 @@ setDocument = Sizzle.setDocument = function( node ) { }); // Check if getElementsByClassName can be trusted - support.getElementsByClassName = assert(function( div ) { + support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) { div.innerHTML = "<div class='a'></div><div class='a i'></div>"; // Support: Safari<4 @@ -1519,7 +1090,7 @@ setDocument = Sizzle.setDocument = function( node ) { var m = context.getElementById( id ); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; + return m && m.parentNode ? [ m ] : []; } }; Expr.filter["ID"] = function( id ) { @@ -1599,7 +1170,15 @@ setDocument = Sizzle.setDocument = function( node ) { // setting a boolean content attribute, // since its presence should be enough // http://bugs.jquery.com/ticket/12359 - div.innerHTML = "<select><option selected=''></option></select>"; + div.innerHTML = "<select msallowclip=''><option selected=''></option></select>"; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowclip^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } // Support: IE8 // Boolean attributes and "value" are not treated correctly @@ -1616,18 +1195,16 @@ setDocument = Sizzle.setDocument = function( node ) { }); assert(function( div ) { - - // Support: Opera 10-12/IE8 - // ^= $= *= and empty values - // Should not select anything // Support: Windows 8 Native Apps - // The type attribute is restricted during .innerHTML assignment + // The type and name attributes are restricted during .innerHTML assignment var input = doc.createElement("input"); input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "t", "" ); + div.appendChild( input ).setAttribute( "name", "D" ); - if ( div.querySelectorAll("[t^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); } // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) @@ -1642,7 +1219,8 @@ setDocument = Sizzle.setDocument = function( node ) { }); } - if ( (support.matchesSelector = rnative.test( (matches = docElem.webkitMatchesSelector || + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || docElem.mozMatchesSelector || docElem.oMatchesSelector || docElem.msMatchesSelector) )) ) { @@ -1664,11 +1242,12 @@ setDocument = Sizzle.setDocument = function( node ) { /* Contains ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another // Purposefully does not implement inclusive descendent // As in, an element does not contain itself - contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? + contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; @@ -1693,7 +1272,7 @@ setDocument = Sizzle.setDocument = function( node ) { ---------------------------------------------------------------------- */ // Document order sorting - sortOrder = docElem.compareDocumentPosition ? + sortOrder = hasCompare ? function( a, b ) { // Flag for duplicate removal @@ -1702,34 +1281,46 @@ setDocument = Sizzle.setDocument = function( node ) { return 0; } - var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); - + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; if ( compare ) { - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + return compare; + } - // Choose the first element that is related to our preferred document - if ( a === doc || contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || contains(preferredDoc, b) ) { - return 1; - } + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; - // Maintain original order - return sortInput ? - ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : - 0; + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; } - return compare & 4 ? -1 : 1; + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; } - // Not directly comparable, sort on existence of method - return a.compareDocumentPosition ? -1 : 1; + return compare & 4 ? -1 : 1; } : function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + var cur, i = 0, aup = a.parentNode, @@ -1737,13 +1328,8 @@ setDocument = Sizzle.setDocument = function( node ) { ap = [ a ], bp = [ b ]; - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { + if ( !aup || !bup ) { return a === doc ? -1 : b === doc ? 1 : aup ? -1 : @@ -1815,7 +1401,7 @@ Sizzle.matchesSelector = function( elem, expr ) { } catch(e) {} } - return Sizzle( expr, document, null, [elem] ).length > 0; + return Sizzle( expr, document, null, [ elem ] ).length > 0; }; Sizzle.contains = function( context, elem ) { @@ -1838,13 +1424,13 @@ Sizzle.attr = function( elem, name ) { fn( elem, name, !documentIsHTML ) : undefined; - return val === undefined ? + return val !== undefined ? + val : support.attributes || !documentIsHTML ? elem.getAttribute( name ) : (val = elem.getAttributeNode(name)) && val.specified ? val.value : - null : - val; + null; }; Sizzle.error = function( msg ) { @@ -1877,6 +1463,10 @@ Sizzle.uniqueSort = function( results ) { } } + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + return results; }; @@ -1892,13 +1482,13 @@ getText = Sizzle.getText = function( elem ) { if ( !nodeType ) { // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { + while ( (node = elem[i++]) ) { // Do not traverse comment nodes ret += getText( node ); } } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) + // innerText usage removed for consistency of new lines (jQuery #11153) if ( typeof elem.textContent === "string" ) { return elem.textContent; } else { @@ -1940,7 +1530,7 @@ Expr = Sizzle.selectors = { match[1] = match[1].replace( runescape, funescape ); // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); if ( match[2] === "~=" ) { match[3] = " " + match[3] + " "; @@ -1983,15 +1573,15 @@ Expr = Sizzle.selectors = { "PSEUDO": function( match ) { var excess, - unquoted = !match[5] && match[2]; + unquoted = !match[6] && match[2]; if ( matchExpr["CHILD"].test( match[0] ) ) { return null; } // Accept quoted arguments as-is - if ( match[3] && match[4] !== undefined ) { - match[2] = match[4]; + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; // Strip excess characters from unquoted arguments } else if ( unquoted && rpseudo.test( unquoted ) && @@ -2295,12 +1885,11 @@ Expr = Sizzle.selectors = { // Contents "empty": function( elem ) { // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + if ( elem.nodeType < 6 ) { return false; } } @@ -2327,11 +1916,12 @@ Expr = Sizzle.selectors = { "text": function( elem ) { var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case return elem.nodeName.toLowerCase() === "input" && elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); }, // Position-in-collection @@ -2396,7 +1986,7 @@ function setFilters() {} setFilters.prototype = Expr.filters = Expr.pseudos; Expr.setFilters = new setFilters(); -function tokenize( selector, parseOnly ) { +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { var matched, match, tokens, type, soFar, groups, preFilters, cached = tokenCache[ selector + " " ]; @@ -2417,7 +2007,7 @@ function tokenize( selector, parseOnly ) { // Don't consume trailing commas as valid soFar = soFar.slice( match[0].length ) || soFar; } - groups.push( tokens = [] ); + groups.push( (tokens = []) ); } matched = false; @@ -2461,7 +2051,7 @@ function tokenize( selector, parseOnly ) { Sizzle.error( selector ) : // Cache the tokens tokenCache( selector, groups ).slice( 0 ); -} +}; function toSelector( tokens ) { var i = 0, @@ -2490,8 +2080,8 @@ function addCombinator( matcher, combinator, base ) { // Check against all ancestor/preceding elements function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching if ( xml ) { @@ -2506,14 +2096,17 @@ function addCombinator( matcher, combinator, base ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { return true; } } @@ -2537,6 +2130,15 @@ function elementMatcher( matchers ) { matchers[0]; } +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + function condense( unmatched, map, filter, context, xml ) { var elem, newUnmatched = [], @@ -2707,31 +2309,30 @@ function matcherFromTokens( tokens ) { } function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, + var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { + superMatcher = function( seed, context, xml, results, outermost ) { var elem, j, matcher, - setMatched = [], matchedCount = 0, i = "0", unmatched = seed && [], - outermost = expandContext != null, + setMatched = [], contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; if ( outermost ) { outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; } // Add elements passing elementMatchers directly to results // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; while ( (matcher = elementMatchers[j++]) ) { @@ -2742,7 +2343,6 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { } if ( outermost ) { dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; } } @@ -2807,7 +2407,7 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { superMatcher; } -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], @@ -2815,12 +2415,12 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { if ( !cached ) { // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); + if ( !match ) { + match = tokenize( selector ); } - i = group.length; + i = match.length; while ( i-- ) { - cached = matcherFromTokens( group[i] ); + cached = matcherFromTokens( match[i] ); if ( cached[ expando ] ) { setMatchers.push( cached ); } else { @@ -2830,82 +2430,91 @@ compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { // Cache the compiled function cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; } return cached; }; -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { var i, tokens, token, type, find, - match = tokenize( selector ); + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - } - selector = selector.slice( tokens.shift().value.length ); + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; } - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; + selector = selector.slice( tokens.shift().value.length ); + } - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; - break; + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; } + + break; } } } } - // Compile and execute a filtering function + // Compile and execute a filtering function if one is not provided // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( + ( compiled || compile( selector, match ) )( seed, context, !documentIsHTML, results, - rsibling.test( selector ) + rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; -} +}; // One-time assignments @@ -2914,7 +2523,7 @@ support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; // Support: Chrome<14 // Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = hasDuplicate; +support.detectDuplicates = !!hasDuplicate; // Initialize against the default document setDocument(); @@ -2962,13 +2571,20 @@ if ( !assert(function( div ) { addHandle( booleans, function( elem, name, isXML ) { var val; if ( !isXML ) { - return (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - elem[ name ] === true ? name.toLowerCase() : null; + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; } }); } +return Sizzle; + +})( window ); + + + jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; jQuery.expr[":"] = jQuery.expr.pseudos; @@ -2978,14 +2594,422 @@ jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; -})( window ); + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; + }); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; + }, + + sibling: function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter(function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.unique( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +}); +var rnotwhite = (/\S+/g); + + + // String to Object options format cache var optionsCache = {}; // Convert String-formatted options into Object-formatted ones and store in cache function createOptions( options ) { var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; }); return object; @@ -3021,18 +3045,18 @@ jQuery.Callbacks = function( options ) { ( optionsCache[ options ] || createOptions( options ) ) : jQuery.extend( {}, options ); - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) + var // Last fire value (for non-forgettable lists) memory, // Flag to know if list was already fired fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, // End of the loop when firing firingLength, // Index of currently firing callback (modified by remove if needed) firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, // Actual callback list list = [], // Stack of fire calls for repeatable lists @@ -3102,7 +3126,7 @@ jQuery.Callbacks = function( options ) { if ( list ) { jQuery.each( arguments, function( _, arg ) { var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { list.splice( index, 1 ); // Handle firing indexes if ( firing ) { @@ -3176,6 +3200,8 @@ jQuery.Callbacks = function( options ) { return self; }; + + jQuery.extend({ Deferred: function( func ) { @@ -3198,8 +3224,7 @@ jQuery.extend({ var fns = arguments; return jQuery.Deferred(function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; // deferred[ done | fail | progress ] for forwarding actions to newDefer deferred[ tuple[1] ](function() { var returned = fn && fn.apply( this, arguments ); @@ -3209,7 +3234,7 @@ jQuery.extend({ .fail( newDefer.reject ) .progress( newDefer.notify ); } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); } }); }); @@ -3268,7 +3293,7 @@ jQuery.extend({ // Deferred helper when: function( subordinate /* , ..., subordinateN */ ) { var i = 0, - resolveValues = core_slice.call( arguments ), + resolveValues = slice.call( arguments ), length = resolveValues.length, // the count of uncompleted subordinates @@ -3281,8 +3306,8 @@ jQuery.extend({ updateFunc = function( i, contexts, values ) { return function( value ) { contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { deferred.notifyWith( contexts, values ); } else if ( !( --remaining ) ) { deferred.resolveWith( contexts, values ); @@ -3317,509 +3342,441 @@ jQuery.extend({ return deferred.promise(); } }); -jQuery.support = (function( support ) { - - var all, a, input, select, fragment, opt, eventName, isSupported, i, - div = document.createElement("div"); - - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - // Finish early in limited (non-browser) environments - all = div.getElementsByTagName("*") || []; - a = div.getElementsByTagName("a")[ 0 ]; - if ( !a || !a.style || !all.length ) { - return support; - } - - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px;float:left;opacity:.5"; - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; - - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName("tbody").length; +// The deferred used on DOM ready +var readyList; - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !!div.getElementsByTagName("link").length; +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test( a.getAttribute("style") ); - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - support.opacity = /^0.5/.test( a.style.opacity ); + return this; +}; - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !!a.style.cssFloat; +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !!input.value; + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, - // Tests for enctype support on a form (#6743) - support.enctype = !!document.createElement("form").enctype; + // Handle when the DOM is ready + ready: function( wait ) { - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav></:nav>"; + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } - // Will be defined later - support.inlineBlockNeedsLayout = false; - support.shrinkWrapBlocks = false; - support.pixelPosition = false; - support.deleteExpando = true; - support.noCloneEvent = true; - support.reliableMarginRight = true; - support.boxSizingReliable = true; + // Remember that the DOM is ready + jQuery.isReady = true; - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } } +}); - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + jQuery.ready(); +} - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); + readyList = jQuery.Deferred(); - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + } else { - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); - div.cloneNode( true ).click(); + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + } } + return readyList.promise( obj ); +}; - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - // Support: IE<9 - // Iteration over object's inherited properties before its own. - for ( i in jQuery( support ) ) { - break; - } - support.ownLast = i !== "0"; - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; - if ( !body ) { - // Return for frameset docs that don't have a body - return; + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); } - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>"; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior. - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + // Sets one value + } else if ( value !== undefined ) { + chainable = true; - // Workaround failing boxSizing test due to offsetWidth returning wrong value - // with some non-1 values of body zoom, ticket #13543 - jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { - support.boxSizing = div.offsetWidth === 4; - }); + if ( !jQuery.isFunction( value ) ) { + raw = true; + } - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); - } - - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "<div></div>"; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; } } - body.removeChild( container ); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; -})({}); - -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; + if ( fn ) { + for ( ; i < len; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } } - var ret, thisCache, - internalKey = jQuery.expando, + return chainable ? + elems : - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[0], key ) : emptyGet; +}; - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( owner ) { + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { - return; - } - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; +function Data() { + // Support: Android < 4, + // Old WebKit does not have Object.preventExtensions/freeze method, + // return new empty object instead with no [[set]] accessor + Object.defineProperty( this.cache = {}, 0, { + get: function() { + return {}; } - } - - if ( !cache[ id ] ) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; - } + }); - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } + this.expando = jQuery.expando + Math.random(); +} - thisCache = cache[ id ]; +Data.uid = 1; +Data.accepts = jQuery.acceptData; - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; +Data.prototype = { + key: function( owner ) { + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return the key for a frozen object. + if ( !Data.accepts( owner ) ) { + return 0; } - thisCache = thisCache.data; - } + var descriptor = {}, + // Check if the owner object already has a cache key + unlock = owner[ this.expando ]; - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( typeof name === "string" ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; + // If not, create one + if ( !unlock ) { + unlock = Data.uid++; - // Test for null|undefined property data - if ( ret == null ) { + // Secure it in a non-enumerable, non-writable property + try { + descriptor[ this.expando ] = { value: unlock }; + Object.defineProperties( owner, descriptor ); - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; + // Support: Android < 4 + // Fallback to a less secure definition + } catch ( e ) { + descriptor[ this.expando ] = unlock; + jQuery.extend( owner, descriptor ); + } } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - if ( name ) { + // Ensure the cache object + if ( !this.cache[ unlock ] ) { + this.cache[ unlock ] = {}; + } - thisCache = pvt ? cache[ id ] : cache[ id ].data; + return unlock; + }, + set: function( owner, data, value ) { + var prop, + // There may be an unlock assigned to this node, + // if there is no entry for this "owner", create one inline + // and set the unlock as though an owner entry had always existed + unlock = this.key( owner ), + cache = this.cache[ unlock ]; - if ( thisCache ) { + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { + // Handle: [ owner, { properties } ] args + } else { + // Fresh assignments by object are shallow copied + if ( jQuery.isEmptyObject( cache ) ) { + jQuery.extend( this.cache[ unlock ], data ); + // Otherwise, copy the properties one-by-one to the cache object + } else { + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + } + return cache; + }, + get: function( owner, key ) { + // Either a valid cache is found, or will be created. + // New caches will be created and the unlock returned, + // allowing direct access to the newly created + // empty data object. A valid owner object must be provided. + var cache = this.cache[ this.key( owner ) ]; + + return key === undefined ? + cache : cache[ key ]; + }, + access: function( owner, key, value ) { + var stored; + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ((key && typeof key === "string") && value === undefined) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase(key) ); + } + + // [*]When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + unlock = this.key( owner ), + cache = this.cache[ unlock ]; - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { + if ( key === undefined ) { + this.cache[ unlock ] = {}; - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { + } else { + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { // If "name" is an array of keys... // When data is initially created, via ("key", "val") signature, // keys will be converted to camelCase. // Since there is no way to tell _how_ a key was added, remove // both plain key and camelCase key. #12786 // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } } i = name.length; while ( i-- ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { - return; + delete cache[ name[ i ] ]; } } + }, + hasData: function( owner ) { + return !jQuery.isEmptyObject( + this.cache[ owner[ this.expando ] ] || {} + ); + }, + discard: function( owner ) { + if ( owner[ this.expando ] ) { + delete this.cache[ owner[ this.expando ] ]; + } } +}; +var data_priv = new Data(); - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; +var data_user = new Data(); - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - /* jshint eqeqeq: true */ - delete cache[ id ]; +/* + Implementation Summary + + 1. Enforce API surface and semantic compatibility with 1.9.x branch + 2. Improve the module's maintainability by reducing the storage + paths to a single mechanism. + 3. Use the same single mechanism to support "private" and "user" data. + 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) + 5. Avoid exposing implementation details on user objects (eg. expando properties) + 6. Provide a clear path for implementation upgrade to WeakMap in 2014 +*/ +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; - // When all else fails, null - } else { - cache[ id ] = null; +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + data_user.set( elem, key, data ); + } else { + data = undefined; + } } + return data; } jQuery.extend({ - cache: {}, - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "applet": true, - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); + return data_user.hasData( elem ) || data_priv.hasData( elem ); }, data: function( elem, name, data ) { - return internalData( elem, name, data ); + return data_user.access( elem, name, data ); }, removeData: function( elem, name ) { - return internalRemoveData( elem, name ); + data_user.remove( elem, name ); }, - // For internal use only. + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to data_priv methods, these can be deprecated. _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); + return data_priv.access( elem, name, data ); }, _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; + data_priv.remove( elem, name ); } }); jQuery.fn.extend({ data: function( key, value ) { - var attrs, name, - data = null, - i = 0, - elem = this[0]; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; // Gets all values if ( key === undefined ) { if ( this.length ) { - data = jQuery.data( elem ); + data = data_user.get( elem ); - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; - - if ( name.indexOf("data-") === 0 ) { - name = jQuery.camelCase( name.slice(5) ); + if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { - dataAttr( elem, name, data[ name ] ); + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } } } - jQuery._data( elem, "parsedAttrs", true ); + data_priv.set( elem, "hasDataAttrs", true ); } } @@ -3829,88 +3786,86 @@ jQuery.fn.extend({ // Sets multiple values if ( typeof key === "object" ) { return this.each(function() { - jQuery.data( this, key ); + data_user.set( this, key ); }); } - return arguments.length > 1 ? + return access( this, function( value ) { + var data, + camelKey = jQuery.camelCase( key ); + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + // Attempt to get data from the cache + // with the key as-is + data = data_user.get( elem, key ); + if ( data !== undefined ) { + return data; + } - // Sets one value - this.each(function() { - jQuery.data( this, key, value ); - }) : + // Attempt to get data from the cache + // with the key camelized + data = data_user.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each(function() { + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = data_user.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + data_user.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf("-") !== -1 && data !== undefined ) { + data_user.set( this, key, value ); + } + }); + }, null, value, arguments.length > 1, null, true ); }, removeData: function( key ) { return this.each(function() { - jQuery.removeData( this, key ); + data_user.remove( this, key ); }); } }); -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} jQuery.extend({ queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); + queue = data_priv.get( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); + if ( !queue || jQuery.isArray( data ) ) { + queue = data_priv.access( elem, type, jQuery.makeArray(data) ); } else { queue.push( data ); } @@ -3957,10 +3912,9 @@ jQuery.extend({ // not intended for public consumption - generates a queueHooks object, or returns the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { + return data_priv.get( elem, key ) || data_priv.access( elem, key, { empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); + data_priv.remove( elem, [ type + "queue", key ] ); }) }); } @@ -3998,19 +3952,6 @@ jQuery.fn.extend({ jQuery.dequeue( this, type ); }); }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, @@ -4034,8 +3975,8 @@ jQuery.fn.extend({ } type = type || "fx"; - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); + while ( i-- ) { + tmp = data_priv.get( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); @@ -4045,668 +3986,54 @@ jQuery.fn.extend({ return defer.promise( obj ); } }); -var nodeHook, boolHook, - rclass = /[\t\r\n\f]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); - - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; - - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - classNames = value.match( core_rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var ret, hooks, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // Use proper attribute retrieval(#6932, #12072) - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - elem.text; - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { - optionSet = true; - } - } - - // force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attr: function( elem, name, value ) { - var hooks, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( jQuery.expr.match.bool.test( name ) ) { - // Set corresponding property to false - if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - elem[ propName ] = false; - // Support: IE<9 - // Also clear defaultChecked/defaultSelected (if appropriate) - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? - ret : - ( elem[ name ] = value ); - - } else { - return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? - ret : - elem[ name ]; - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - return tabindex ? - parseInt( tabindex, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - -1; - } - } - } -}); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; - - jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? - function( elem, name, isXML ) { - var fn = jQuery.expr.attrHandle[ name ], - ret = isXML ? - undefined : - /* jshint eqeqeq: false */ - (jQuery.expr.attrHandle[ name ] = undefined) != - getter( elem, name, isXML ) ? - - name.toLowerCase() : - null; - jQuery.expr.attrHandle[ name ] = fn; - return ret; - } : - function( elem, name, isXML ) { - return isXML ? - undefined : - elem[ jQuery.camelCase( "default-" + name ) ] ? - name.toLowerCase() : - null; - }; -}); - -// fix oldIE attroperties -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = { - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } +var rcheckableType = (/^(?:checkbox|radio)$/i); - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute( name ) ? - value : - undefined; - } - }; - jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = - // Some attributes are constructed with empty-string values when not defined - function( elem, name, isXML ) { - var ret; - return isXML ? - undefined : - (ret = elem.getAttributeNode( name )) && ret.value !== "" ? - ret.value : - null; - }; - jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ret.specified ? - ret.value : - undefined; - }, - set: nodeHook.set - }; - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }; - }); -} +(function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + // #11217 - WebKit loses check when the name is after the checked attribute + // Support: Windows Web Apps (WWA) + // `name` and `type` need .setAttribute for WWA + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !jQuery.support.hrefNormalized ) { - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} + div.appendChild( input ); -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE9-IE11+ + div.innerHTML = "<textarea>x</textarea>"; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +})(); +var strundefined = typeof undefined; - if ( parent ) { - parent.selectedIndex; - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }; -} -jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -}); +support.focusinBubbles = "onfocusin" in window; -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} -// Radios and checkboxes getter/setter -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }; - if ( !jQuery.support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } -}); -var rformElems = /^(?:input|select|textarea)$/i, +var rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; @@ -4733,10 +4060,11 @@ jQuery.event = { global: {}, add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.get( elem ); // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { @@ -4763,16 +4091,13 @@ jQuery.event = { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; + return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; } // Handle multiple events separated by a space - types = ( types || "" ).match( core_rnotwhite ) || [""]; + types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[t] ) || []; @@ -4810,14 +4135,10 @@ jQuery.event = { handlers = events[ type ] = []; handlers.delegateCount = 0; - // Only use addEventListener/attachEvent if the special events handler returns false + // Only use addEventListener if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); } } } @@ -4841,24 +4162,22 @@ jQuery.event = { jQuery.event.global[ type ] = true; } - // Nullify elem to prevent memory leaks in IE - elem = null; }, // Detach an event or set of events from an element remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.hasData( elem ) && data_priv.get( elem ); if ( !elemData || !(events = elemData.events) ) { return; } // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; + types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { tmp = rtypenamespace.exec( types[t] ) || []; @@ -4912,19 +4231,16 @@ jQuery.event = { // Remove the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); + data_priv.remove( elem, "events" ); } }, trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, + + var i, cur, tmp, bubbleType, ontype, handle, special, eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; cur = tmp = elem = elem || document; @@ -5003,15 +4319,18 @@ jQuery.event = { special.bindType || type; // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); } // Native handler handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } } } event.type = type; @@ -5023,9 +4342,8 @@ jQuery.event = { jQuery.acceptData( elem ) ) { // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { + if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method tmp = elem[ ontype ]; @@ -5036,12 +4354,7 @@ jQuery.event = { // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } + elem[ type ](); jQuery.event.triggered = undefined; if ( tmp ) { @@ -5059,10 +4372,10 @@ jQuery.event = { // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event ); - var i, ret, handleObj, matched, j, + var i, j, ret, matched, handleObj, handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], + args = slice.call( arguments ), + handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event @@ -5114,7 +4427,7 @@ jQuery.event = { }, handlers: function( event, handlers ) { - var sel, handleObj, matches, i, + var i, matches, sel, handleObj, handlerQueue = [], delegateCount = handlers.delegateCount, cur = event.target; @@ -5124,13 +4437,10 @@ jQuery.event = { // Avoid non-left-click bubbling in Firefox (#3861) if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - /* jshint eqeqeq: false */ - for ( ; cur != this; cur = cur.parentNode || this ) { - /* jshint eqeqeq: true */ + for ( ; cur !== this; cur = cur.parentNode || this ) { - // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { + if ( cur.disabled !== true || event.type !== "click" ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; @@ -5162,52 +4472,6 @@ jQuery.event = { return handlerQueue; }, - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - // Includes some event props shared by KeyEvent and MouseEvent props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), @@ -5227,11 +4491,10 @@ jQuery.event = { }, mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; + var eventDoc, doc, body, + button = original.button; // Calculate pageX/Y if missing and clientX/Y available if ( event.pageX == null && original.clientX != null ) { @@ -5243,11 +4506,6 @@ jQuery.event = { event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); } - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - // Add which for click: 1 === left; 2 === middle; 3 === right // Note: button is not normalized, so don't use it if ( !event.which && button !== undefined ) { @@ -5258,6 +4516,48 @@ jQuery.event = { } }, + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome < 28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + special: { load: { // Prevent triggered image.load events from bubbling to window.load @@ -5267,14 +4567,8 @@ jQuery.event = { // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } + this.focus(); + return false; } }, delegateType: "focusin" @@ -5291,7 +4585,7 @@ jQuery.event = { click: { // For checkbox, fire native event so checked state will be right trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { this.click(); return false; } @@ -5306,8 +4600,9 @@ jQuery.event = { beforeunload: { postDispatch: function( event ) { - // Even when returnValue equals to undefined Firefox will still show alert - if ( event.result !== undefined ) { + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { event.originalEvent.returnValue = event.result; } } @@ -5338,26 +4633,11 @@ jQuery.event = { } }; -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === core_strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; +jQuery.removeEvent = function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } +}; jQuery.Event = function( src, props ) { // Allow instantiation without the 'new' keyword @@ -5372,8 +4652,12 @@ jQuery.Event = function( src, props ) { // Events bubbling up the document may have been marked as prevented // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: Android < 4.0 + src.returnValue === false ? + returnTrue : + returnFalse; // Event type } else { @@ -5403,46 +4687,40 @@ jQuery.Event.prototype = { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { + if ( e && e.preventDefault ) { e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; } }, stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { + + if ( e && e.stopPropagation ) { e.stopPropagation(); } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; }, stopImmediatePropagation: function() { + var e = this.originalEvent; + this.isImmediatePropagationStopped = returnTrue; + + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); + } + this.stopPropagation(); } }; // Create mouseenter/leave events using mouseover/out and event-time checks +// Support: Chrome 15+ jQuery.each({ mouseenter: "mouseover", - mouseleave: "mouseout" + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" }, function( orig, fix ) { jQuery.event.special[ orig ] = { delegateType: fix, @@ -5466,131 +4744,36 @@ jQuery.each({ }; }); -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - // Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { +// Support: Firefox, Chrome, Safari +if ( !support.focusinBubbles ) { jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); }; jQuery.event.special[ fix ] = { setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); } + data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); }, teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + data_priv.remove( doc, fix ); + + } else { + data_priv.access( doc, fix, attaches ); } } }; @@ -5600,7 +4783,7 @@ if ( !jQuery.support.focusinBubbles ) { jQuery.fn.extend({ on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; + var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { @@ -5699,348 +4882,311 @@ jQuery.fn.extend({ } } }); -var isSimple = /^.[^:#\[\.,]*$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; -jQuery.fn.extend({ - find: function( selector ) { - var i, - ret = [], - self = this, - len = self.length; - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } + // We have to close these tags to support XHTML (#13200) + wrapMap = { - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, + // Support: IE 9 + option: [ 1, "<select multiple='multiple'>", "</select>" ], - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; + thead: [ 1, "<table>", "</table>" ], + col: [ 2, "<table><colgroup>", "</colgroup></table>" ], + tr: [ 2, "<table><tbody>", "</tbody></table>" ], + td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, + _default: [ 0, "", "" ] + }; - not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); - }, +// Support: IE 9 +wrapMap.optgroup = wrapMap.option; - filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); - }, +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; - is: function( selector ) { - return !!winnow( - this, +// Support: 1.x compatibility +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - }, + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - ret = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); - for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { - // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute("type"); + } - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { + return elem; +} - cur = ret.push( cur ); - break; - } - } - } +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; - return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); - }, + for ( ; i < l; i++ ) { + data_priv.set( + elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) + ); + } +} - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } + if ( dest.nodeType !== 1 ) { + return; + } - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } + // 1. Copy private data: events, handlers, etc. + if ( data_priv.hasData( src ) ) { + pdataOld = data_priv.access( src ); + pdataCur = data_priv.set( dest, pdataOld ); + events = pdataOld.events; - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } - return this.pushStack( jQuery.unique(all) ); - }, + // 2. Copy user data + if ( data_user.hasData( src ) ) { + udataOld = data_user.access( src ); + udataCur = jQuery.extend( {}, udataOld ); - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); + data_user.set( dest, udataCur ); } -}); +} -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); +function getAll( context, tag ) { + var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : + context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : + []; - return cur; + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; } -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); +// Support: IE >= 9 +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); +} - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } + // Support: IE >= 9 + // Fix Cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { - if ( this.length > 1 ) { - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - ret = jQuery.unique( ret ); - } + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - ret = ret.reverse(); + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); } } - return this.pushStack( ret ); - }; -}); + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); -jQuery.extend({ - filter: function( expr, elems, not ) { - var elem = elems[ 0 ]; + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } - if ( not ) { - expr = ":not(" + expr + ")"; + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); } - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : - jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - })); + // Return the cloned set + return clone; }, - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; + buildFragment: function( elems, context, scripts, selection ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, + for ( ; i < l; i++ ) { + elem = elems[ i ]; - sibling: function( n, elem ) { - var r = []; + if ( elem || elem === 0 ) { - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - return r; - } -}); + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - /* jshint -W018 */ - return !!qualifier.call( elem, i, elem ) !== not; - }); + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement("div") ); - } + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ]; - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - }); + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } - } + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, tmp.childNodes ); - if ( typeof qualifier === "string" ) { - if ( isSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); + // Remember the top-level container + tmp = fragment.firstChild; + + // Fixes #12346 + // Support: Webkit, IE + tmp.textContent = ""; + } + } } - qualifier = jQuery.filter( qualifier, elements ); - } + // Remove wrapper from fragment + fragment.textContent = ""; - return jQuery.grep( elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; - }); -} -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); + i = 0; + while ( (elem = nodes[ i++ ]) ) { - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /<tbody/i, - rhtml = /<|&#?\w+;/, - rnoInnerhtml = /<(?:script|style|link)/i, - manipulation_rcheckableType = /^(?:checkbox|radio)$/i, - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /^$|\/(?:java|ecma)script/i, - rscriptTypeMasked = /^true\/(.*)/, - rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, + contains = jQuery.contains( elem.ownerDocument, elem ); - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "<select multiple='multiple'>", "</select>" ], - legend: [ 1, "<fieldset>", "</fieldset>" ], - area: [ 1, "<map>", "</map>" ], - param: [ 1, "<object>", "</object>" ], - thead: [ 1, "<table>", "</table>" ], - tr: [ 2, "<table><tbody>", "</tbody></table>" ], - col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], - td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ] + return fragment; }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; + cleanData: function( elems ) { + var data, elem, type, key, + special = jQuery.event.special, + i = 0; + + for ( ; (elem = elems[ i ]) !== undefined; i++ ) { + if ( jQuery.acceptData( elem ) ) { + key = elem[ data_priv.expando ]; + + if ( key && (data = data_priv.cache[ key ]) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + if ( data_priv.cache[ key ] ) { + // Discard any remaining `private` data + delete data_priv.cache[ key ]; + } + } + } + // Discard any remaining `user` data + delete data_user.cache[ elem[ data_user.expando ] ]; + } + } +}); jQuery.fn.extend({ text: function( value ) { - return jQuery.access( this, function( value ) { + return access( this, function( value ) { return value === undefined ? jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); + this.empty().each(function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + }); }, null, value, arguments.length ); }, @@ -6078,14 +5224,12 @@ jQuery.fn.extend({ }); }, - // keepData is for internal use only--do not document - remove: function( selector, keepData ) { + remove: function( selector, keepData /* Internal Use Only */ ) { var elem, elems = selector ? jQuery.filter( selector, this ) : this, i = 0; for ( ; (elem = elems[i]) != null; i++ ) { - if ( !keepData && elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem ) ); } @@ -6106,20 +5250,13 @@ jQuery.fn.extend({ i = 0; for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; + // Remove any remaining nodes + elem.textContent = ""; } } @@ -6130,35 +5267,32 @@ jQuery.fn.extend({ dataAndEvents = dataAndEvents == null ? false : dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - return this.map( function () { + return this.map(function() { return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); }); }, html: function( value ) { - return jQuery.access( this, function( value ) { - var elem = this[0] || {}, + return access( this, function( value ) { + var elem = this[ 0 ] || {}, i = 0, l = this.length; - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; } // See if we can take a shortcut and just use innerHTML if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { value = value.replace( rxhtmlTag, "<$1></$2>" ); try { - for (; i < l; i++ ) { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + // Remove element nodes and prevent memory leaks - elem = this[i] || {}; if ( elem.nodeType === 1 ) { jQuery.cleanData( getAll( elem, false ) ); elem.innerHTML = value; @@ -6168,7 +5302,7 @@ jQuery.fn.extend({ elem = 0; // If using innerHTML throws an exception, use the fallback method - } catch(e) {} + } catch( e ) {} } if ( elem ) { @@ -6178,64 +5312,55 @@ jQuery.fn.extend({ }, replaceWith: function() { - var - // Snapshot the DOM in case .domManip sweeps something relevant into its fragment - args = jQuery.map( this, function( elem ) { - return [ elem.nextSibling, elem.parentNode ]; - }), - i = 0; + var arg = arguments[ 0 ]; // Make the changes, replacing each context element with the new content this.domManip( arguments, function( elem ) { - var next = args[ i++ ], - parent = args[ i++ ]; + arg = this.parentNode; - if ( parent ) { - // Don't use the snapshot next if it has moved (#13810) - if ( next && next.parentNode !== parent ) { - next = this.nextSibling; - } - jQuery( this ).remove(); - parent.insertBefore( elem, next ); + jQuery.cleanData( getAll( this ) ); + + if ( arg ) { + arg.replaceChild( elem, this ); } - // Allow new content to include elements from the context set - }, true ); + }); // Force removal if there was no new content (e.g., from empty arguments) - return i ? this : this.remove(); + return arg && (arg.length || arg.nodeType) ? this : this.remove(); }, detach: function( selector ) { return this.remove( selector, true ); }, - domManip: function( args, callback, allowIntersection ) { + domManip: function( args, callback ) { // Flatten any nested arrays - args = core_concat.apply( [], args ); + args = concat.apply( [], args ); - var first, node, hasScripts, - scripts, doc, fragment, + var fragment, first, scripts, hasScripts, node, doc, i = 0, l = this.length, set = this, iNoClone = l - 1, - value = args[0], + value = args[ 0 ], isFunction = jQuery.isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { return this.each(function( index ) { var self = set.eq( index ); if ( isFunction ) { - args[0] = value.call( this, index, self.html() ); + args[ 0 ] = value.call( this, index, self.html() ); } - self.domManip( args, callback, allowIntersection ); + self.domManip( args, callback ); }); } if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && this ); + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { @@ -6256,11 +5381,13 @@ jQuery.fn.extend({ // Keep references to cloned scripts for later restoration if ( hasScripts ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws jQuery.merge( scripts, getAll( node, "script" ) ); } } - callback.call( this[i], node, i ); + callback.call( this[ i ], node, i ); } if ( hasScripts ) { @@ -6273,20 +5400,19 @@ jQuery.fn.extend({ for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { - // Hope ajax is available... - jQuery._evalUrl( node.src ); + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); } } } } - - // Fix #11809: Avoid leaking memory - fragment = first = null; } } @@ -6294,136 +5420,6 @@ jQuery.fn.extend({ } }); -// Support: IE<8 -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType === 1 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild( elem.ownerDocument.createElement("tbody") ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - jQuery.each({ appendTo: "append", prependTo: "prepend", @@ -6433,385 +5429,283 @@ jQuery.each({ }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, - i = 0, ret = [], insert = jQuery( selector ), - last = insert.length - 1; + last = insert.length - 1, + i = 0; for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - core_push.apply( ret, elems.get() ); + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); } return this.pushStack( ret ); }; }); -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( manipulation_rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); +var iframe, + elemdisplay = {}; - if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); - destElements = srcElements = node = null; + return display; +} - // Return the cloned set - return clone; - }, +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, + if ( !display ) { + display = actualDisplay( nodeName, doc ); - // Ensure a safe fragment - safe = createSafeFragment( context ), + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { - nodes = [], - i = 0; + // Use the already-created iframe if possible + iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement ); - for ( ; i < l; i++ ) { - elem = elems[ i ]; + // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse + doc = iframe[ 0 ].contentDocument; - if ( elem || elem === 0 ) { + // Support: IE + doc.write(); + doc.close(); - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + display = actualDisplay( nodeName, doc ); + iframe.detach(); + } - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); + // Store the correct default display + elemdisplay[ nodeName ] = display; + } - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); + return display; +} +var rmargin = (/^margin/); - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2]; +var getStyles = function( elem ) { + return elem.ownerDocument.defaultView.getComputedStyle( elem, null ); + }; - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - // Manually add leading whitespace removed by IE - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - // Remove IE's autoinserted <tbody> from table fragments - if ( !jQuery.support.tbody ) { +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + style = elem.style; - // String was a <table>, *may* have spurious <tbody> - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : + computed = computed || getStyles( elem ); - // String was a bare <thead> or <tfoot> - wrap[1] === "<table>" && !rtbody.test( elem ) ? - tmp : - 0; + // Support: IE9 + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + if ( computed ) { + ret = computed.getPropertyValue( name ) || computed[ name ]; + } - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } + if ( computed ) { - jQuery.merge( nodes, tmp.childNodes ); + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; + // Support: iOS < 6 + // A tribute to the "awesome hack by Dean Edwards" + // iOS < 6 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; } + } - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !jQuery.support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } + return ret !== undefined ? + // Support: IE + // IE returns zIndex value as an integer. + ret + "" : + ret; +} - i = 0; - while ( (elem = nodes[ i++ ]) ) { - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; +function addGetHookIf( conditionFn, hookFn ) { + // Define the hook, we'll check on the first run if it's really needed. + return { + get: function() { + if ( conditionFn() ) { + // Hook not needed (or it's not possible to use it due to missing dependency), + // remove it. + // Since there are no other hooks for marginRight, remove the whole object. + delete this.get; + return; } - contains = jQuery.contains( elem.ownerDocument, elem ); + // Hook needed; redefine it so that the support test is not executed again. - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } + return (this.get = hookFn).apply( this, arguments ); } + }; +} - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = jQuery.support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } +(function() { + var pixelPositionVal, boxSizingReliableVal, + docElem = document.documentElement, + container = document.createElement( "div" ), + div = document.createElement( "div" ); - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { + if ( !div.style ) { + return; + } - delete cache[ id ]; + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; + container.style.cssText = "border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;" + + "position:absolute"; + container.appendChild( div ); + + // Executing both pixelPosition & boxSizingReliable tests require only one layout + // so they're executed at the same time to save the second computation. + function computePixelPositionAndBoxSizingReliable() { + div.style.cssText = + // Support: Firefox<29, Android 2.3 + // Vendor-prefix box-sizing + "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" + + "box-sizing:border-box;display:block;margin-top:1%;top:1%;" + + "border:1px;padding:1px;width:4px;position:absolute"; + div.innerHTML = ""; + docElem.appendChild( container ); - } else if ( typeof elem.removeAttribute !== core_strundefined ) { - elem.removeAttribute( internalKey ); + var divStyle = window.getComputedStyle( div, null ); + pixelPositionVal = divStyle.top !== "1%"; + boxSizingReliableVal = divStyle.width === "4px"; - } else { - elem[ internalKey ] = null; - } + docElem.removeChild( container ); + } - core_deletedIds.push( id ); - } + // Support: node.js jsdom + // Don't assume that getComputedStyle is a property of the global object + if ( window.getComputedStyle ) { + jQuery.extend( support, { + pixelPosition: function() { + // This test is executed only once but we still do memoizing + // since we can use the boxSizingReliable pre-computing. + // No need to check if the test was already performed, though. + computePixelPositionAndBoxSizingReliable(); + return pixelPositionVal; + }, + boxSizingReliable: function() { + if ( boxSizingReliableVal == null ) { + computePixelPositionAndBoxSizingReliable(); } - } - } - }, + return boxSizingReliableVal; + }, + reliableMarginRight: function() { + // Support: Android 2.3 + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // This support function is only executed once so no memoizing is needed. + var ret, + marginDiv = div.appendChild( document.createElement( "div" ) ); + + // Reset CSS: box-sizing; display; margin; border; padding + marginDiv.style.cssText = div.style.cssText = + // Support: Firefox<29, Android 2.3 + // Vendor-prefix box-sizing + "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + + "box-sizing:content-box;display:block;margin:0;border:0;padding:0"; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + docElem.appendChild( container ); + + ret = !parseFloat( window.getComputedStyle( marginDiv, null ).marginRight ); + + docElem.removeChild( container ); - _evalUrl: function( url ) { - return jQuery.ajax({ - url: url, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true + return ret; + } }); } -}); -jQuery.fn.extend({ - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); +})(); - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - wrap.map(function() { - var elem = this; +// A method for quickly swapping in/out CSS properties to get correct calculations. +jQuery.swap = function( elem, options, callback, args ) { + var ret, name, + old = {}; - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } - if ( contents.length ) { - contents.wrapAll( html ); + ret = callback.apply( elem, args || [] ); - } else { - self.append( html ); - } - }); - }, + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); + return ret; +}; - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - } -}); -var iframe, getStyles, curCSS, - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - rposition = /^(top|right|bottom|left)$/, +var // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rmargin = /^margin/, - rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), - rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), - elemdisplay = { BODY: "block" }, + rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ), cssShow = { position: "absolute", visibility: "hidden", display: "block" }, cssNormalTransform = { - letterSpacing: 0, - fontWeight: 400 + letterSpacing: "0", + fontWeight: "400" }, - cssExpand = [ "Top", "Right", "Bottom", "Left" ], cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; // return a css property mapped to a potentially vendor prefixed property @@ -6823,7 +5717,7 @@ function vendorPropName( style, name ) { } // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), + var capName = name[0].toUpperCase() + name.slice(1), origName = name, i = cssPrefixes.length; @@ -6837,11 +5731,95 @@ function vendorPropName( style, name ) { return origName; } -function isHidden( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && + ( support.boxSizingReliable() || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; } function showHide( elements, show ) { @@ -6856,7 +5834,7 @@ function showHide( elements, show ) { continue; } - values[ index ] = jQuery._data( elem, "olddisplay" ); + values[ index ] = data_priv.get( elem, "olddisplay" ); display = elem.style.display; if ( show ) { // Reset the inline display of this element to learn if it is @@ -6869,16 +5847,13 @@ function showHide( elements, show ) { // in a stylesheet to whatever the default browser style is // for such an element if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + values[ index ] = data_priv.access( elem, "olddisplay", defaultDisplay(elem.nodeName) ); } } else { + hidden = isHidden( elem ); - if ( !values[ index ] ) { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } + if ( display !== "none" || !hidden ) { + data_priv.set( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); } } } @@ -6898,50 +5873,6 @@ function showHide( elements, show ) { return elements; } -jQuery.fn.extend({ - css: function( name, value ) { - return jQuery.access( this, function( elem, name, value ) { - var len, styles, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each(function() { - if ( isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - jQuery.extend({ // Add in style property hooks for overriding the default // behavior of getting and setting a style property @@ -6961,6 +5892,8 @@ jQuery.extend({ cssNumber: { "columnCount": true, "fillOpacity": true, + "flexGrow": true, + "flexShrink": true, "fontWeight": true, "lineHeight": true, "opacity": true, @@ -6975,7 +5908,7 @@ jQuery.extend({ // setting or getting the value cssProps: { // normalize float css property - "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + "float": "cssFloat" }, // Get and set the style property on a DOM Node @@ -7007,8 +5940,8 @@ jQuery.extend({ type = "number"; } - // Make sure that NaN and null values aren't set. See: #7116 - if ( value == null || type === "number" && isNaN( value ) ) { + // Make sure that null and NaN values aren't set. See: #7116 + if ( value == null || value !== value ) { return; } @@ -7017,20 +5950,15 @@ jQuery.extend({ value += "px"; } - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // Fixes #8908, it can be done more correctly by specifying setters in cssHooks, // but it would mean to define eight (for every problematic property) identical functions - if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { style[ name ] = "inherit"; } // If a hook was provided, use that value, otherwise just set the specified value if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Wrapped to prevent IE from throwing errors when 'invalid' values are provided - // Fixes bug #5509 - try { - style[ name ] = value; - } catch(e) {} + style[ name ] = value; } } else { @@ -7045,7 +5973,7 @@ jQuery.extend({ }, css: function( elem, name, extra, styles ) { - var num, val, hooks, + var val, num, hooks, origName = jQuery.camelCase( name ); // Make sure that we're working with the right name @@ -7079,479 +6007,1449 @@ jQuery.extend({ } }); -// NOTE: we've included the "window" in window.getComputedStyle -// because jsdom on node.js will break without it. -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - return window.getComputedStyle( elem, null ); +jQuery.each([ "height", "width" ], function( i, name ) { + jQuery.cssHooks[ name ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + // certain elements can have dimension info if we invisibly show them + // however, it must have a current display style that would benefit from this + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ? + jQuery.swap( elem, cssShow, function() { + return getWidthOrHeight( elem, name, extra ); + }) : + getWidthOrHeight( elem, name, extra ); + } + }, + + set: function( elem, value, extra ) { + var styles = extra && getStyles( elem ); + return setPositiveNumber( elem, value, extra ? + augmentWidthOrHeight( + elem, + name, + extra, + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + styles + ) : 0 + ); + } }; +}); - curCSS = function( elem, name, _computed ) { - var width, minWidth, maxWidth, - computed = _computed || getStyles( elem ), +// Support: Android 2.3 +jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, + function( elem, computed ) { + if ( computed ) { + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + // Work around by temporarily setting element display to inline-block + return jQuery.swap( elem, { "display": "inline-block" }, + curCSS, [ elem, "marginRight" ] ); + } + } +); - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, - style = elem.style; +// These hooks are used by animate to expand properties +jQuery.each({ + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, - if ( computed ) { + // assumes a single number if not a string + parts = typeof value === "string" ? value.split(" ") : [ value ]; - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; } - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; + return expanded; + } + }; + + if ( !rmargin.test( prefix ) ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +}); + +jQuery.fn.extend({ + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); } - return ret; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; + return this.each(function() { + if ( isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); - curCSS = function( elem, name, _computed ) { - var left, rs, rsLeft, - computed = _computed || getStyles( elem ), - ret = computed ? computed[ name ] : undefined, - style = elem.style; - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || "swing"; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; } + this.now = ( this.end - this.start ) * eased + this.start; - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; +Tween.prototype.init.prototype = Tween.prototype; - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem[ tween.prop ] != null && + (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { + return tween.elem[ tween.prop ]; } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; + // passing an empty string as a 3rd parameter to .css will automatically + // attempt a parseFloat and fallback to a string if the parse fails + // so, simple values such as "10px" are parsed to Float. + // complex values such as "rotate(1rad)" are returned as is. + result = jQuery.css( tween.elem, tween.prop, "" ); + // Empty strings, null, undefined and "auto" are converted to 0. + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + // use step hook for back compat - use cssHook if its there - use .style if its + // available and use plain properties where available + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; } } + } +}; + +// Support: IE9 +// Panic based approach to setting things on disconnected nodes + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + } +}; + +jQuery.fx = Tween.prototype.init; + +// Back Compat <1.8 extension point +jQuery.fx.step = {}; + + + + +var + fxNow, timerId, + rfxtypes = /^(?:toggle|show|hide)$/, + rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ), + rrun = /queueHooks$/, + animationPrefilters = [ defaultPrefilter ], + tweeners = { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ), + target = tween.cur(), + parts = rfxnum.exec( value ), + unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && + rfxnum.exec( jQuery.css( tween.elem, prop ) ), + scale = 1, + maxIterations = 20; + + if ( start && start[ 3 ] !== unit ) { + // Trust units reported by jQuery.css + unit = unit || start[ 3 ]; + + // Make sure we update the tween properties later on + parts = parts || []; + + // Iteratively approximate from a nonzero starting point + start = +target || 1; + + do { + // If previous iteration zeroed out, double until we get *something* + // Use a string for doubling factor so we don't accidentally see scale as unchanged below + scale = scale || ".5"; - return ret === "" ? "auto" : ret; + // Adjust and apply + start = start / scale; + jQuery.style( tween.elem, prop, start + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // And breaking the loop if scale is unchanged or perfect, or if we've just had enough + } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); + } + + // Update tween properties + if ( parts ) { + start = tween.start = +start || +target || 0; + tween.unit = unit; + // If a +=/-= token was provided, we're doing a relative animation + tween.end = parts[ 1 ] ? + start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : + +parts[ 2 ]; + } + + return tween; + } ] }; + +// Animations created synchronously will run synchronously +function createFxNow() { + setTimeout(function() { + fxNow = undefined; + }); + return ( fxNow = jQuery.now() ); } -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; +// Generate parameters to create a standard animation +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + // if we include width, step value is 1 to do all cssExpand values, + // if we don't include width, step value is 2 to skip over Left and Right + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4 ; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; } -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, +function createTween( value, prop, animation ) { + var tween, + collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( (tween = collection[ index ].call( animation, prop, value )) ) { - val = 0; + // we're done with this property + return tween; + } + } +} - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); +function defaultPrefilter( elem, props, opts ) { + /* jshint validthis: true */ + var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHidden( elem ), + dataShow = data_priv.get( elem, "fxshow" ); + + // handle queue: false promises + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; } + hooks.unqueued++; - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + anim.always(function() { + // doing this makes sure that the complete handler will be called + // before this completes + anim.always(function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + }); + }); + } + + // height/width overflow pass + if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { + // Make sure that nothing sneaks out + // Record all 3 overflow attributes because IE9-10 do not + // change the overflow attribute when overflowX and + // overflowY are set to the same value + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + // Set display property to inline-block for height/width + // animations on inline elements that are having width/height animated + display = jQuery.css( elem, "display" ); + + // Test default display if display is currently "none" + checkDisplay = display === "none" ? + data_priv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display; + + if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) { + style.display = "inline-block"; + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always(function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + }); + } + + // show/hide pass + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.exec( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + } else { + continue; + } } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + // Any non-fx value stops us from restoring the original display value + } else { + display = undefined; + } + } + + if ( !jQuery.isEmptyObject( orig ) ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; } } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + dataShow = data_priv.access( elem, "fxshow", {} ); + } - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + // store state if its toggle - enables .stop().toggle() to "reverse" + if ( toggle ) { + dataShow.hidden = !hidden; + } + if ( hidden ) { + jQuery( elem ).show(); + } else { + anim.done(function() { + jQuery( elem ).hide(); + }); + } + anim.done(function() { + var prop; + + data_priv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + }); + for ( prop in orig ) { + tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = tween.start; + if ( hidden ) { + tween.end = tween.start; + tween.start = prop === "width" || prop === "height" ? 1 : 0; + } } } - } - return val; + // If this is a noop like .hide().hide(), restore an overwritten display value + } else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) { + style.display = display; + } } -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; + // camelCase, specialEasing and expand cssHook pass + for ( index in props ) { + name = jQuery.camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( jQuery.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; } - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; } - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; + // not quite $.extend, this wont overwrite keys already present. + // also - reusing 'index' from above because we have the correct "name" + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; } -// Try to determine the default display value of an element -function css_defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = animationPrefilters.length, + deferred = jQuery.Deferred().always( function() { + // don't match elem in the :animated selector + delete tick.elem; + }), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; - if ( !display ) { - display = actualDisplay( nodeName, doc ); + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( percent ); + } - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - // Use the already-created iframe if possible - iframe = ( iframe || - jQuery("<iframe frameborder='0' width='0' height='0'/>") - .css( "cssText", "display:block !important" ) - ).appendTo( doc.documentElement ); + deferred.notifyWith( elem, [ animation, percent, remaining ]); - // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse - doc = ( iframe[0].contentWindow || iframe[0].contentDocument ).document; - doc.write("<!doctype html><html><body>"); - doc.close(); + if ( percent < 1 && length ) { + return remaining; + } else { + deferred.resolveWith( elem, [ animation ] ); + return false; + } + }, + animation = deferred.promise({ + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { specialEasing: {} }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + // if we are going to the end, we want to run all the tweens + // otherwise we skip this part + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length ; index++ ) { + animation.tweens[ index ].run( 1 ); + } - display = actualDisplay( nodeName, doc ); - iframe.detach(); + // resolve when we played the last frame + // otherwise, reject + if ( gotoEnd ) { + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + }), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length ; index++ ) { + result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + return result; } + } - // Store the correct default display - elemdisplay[ nodeName ] = display; + jQuery.map( props, createTween, animation ); + + if ( jQuery.isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); } - return display; -} + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + }) + ); -// Called ONLY from within css_defaultDisplay -function actualDisplay( name, doc ) { - var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - display = jQuery.css( elem[0], "display" ); - elem.remove(); - return display; + // attach callbacks from options + return animation.progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); } -jQuery.each([ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - // certain elements can have dimension info if we invisibly show them - // however, it must have a current display style that would benefit from this - return elem.offsetWidth === 0 && rdisplayswap.test( jQuery.css( elem, "display" ) ) ? - jQuery.swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); - }) : - getWidthOrHeight( elem, name, extra ); - } - }, +jQuery.Animation = jQuery.extend( Animation, { - set: function( elem, value, extra ) { - var styles = extra && getStyles( elem ); - return setPositiveNumber( elem, value, extra ? - augmentWidthOrHeight( - elem, - name, - extra, - jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - styles - ) : 0 - ); + tweener: function( props, callback ) { + if ( jQuery.isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.split(" "); } - }; + + var prop, + index = 0, + length = props.length; + + for ( ; index < length ; index++ ) { + prop = props[ index ]; + tweeners[ prop ] = tweeners[ prop ] || []; + tweeners[ prop ].unshift( callback ); + } + }, + + prefilter: function( callback, prepend ) { + if ( prepend ) { + animationPrefilters.unshift( callback ); + } else { + animationPrefilters.push( callback ); + } + } }); -if ( !jQuery.support.opacity ) { - jQuery.cssHooks.opacity = { - get: function( elem, computed ) { - // IE uses filters for opacity - return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? - ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : - computed ? "1" : ""; - }, +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + jQuery.isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + }; - set: function( elem, value ) { - var style = elem.style, - currentStyle = elem.currentStyle, - opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", - filter = currentStyle && currentStyle.filter || style.filter || ""; - - // IE has trouble with opacity if it does not have layout - // Force it by setting the zoom level - style.zoom = 1; - - // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 - // if value === "", then remove inline opacity #12685 - if ( ( value >= 1 || value === "" ) && - jQuery.trim( filter.replace( ralpha, "" ) ) === "" && - style.removeAttribute ) { - - // Setting style.filter to null, "" & " " still leave "filter:" in the cssText - // if "filter:" is present at all, clearType is disabled, we want to avoid this - // style.removeAttribute is IE Only, but so apparently is this code path... - style.removeAttribute( "filter" ); - - // if there is no filter style applied in a css rule or unset inline opacity, we are done - if ( value === "" || currentStyle && !currentStyle.filter ) { - return; - } - } + opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : + opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - // otherwise, set new filter values - style.filter = ralpha.test( filter ) ? - filter.replace( ralpha, opacity ) : - filter + " " + opacity; + // normalize opt.queue - true/undefined/null -> "fx" + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + // Queueing + opt.old = opt.complete; + + opt.complete = function() { + if ( jQuery.isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); } }; -} -// These hooks cannot be added until DOM ready because the support test -// for it is not run until after DOM ready -jQuery(function() { - if ( !jQuery.support.reliableMarginRight ) { - jQuery.cssHooks.marginRight = { - get: function( elem, computed ) { - if ( computed ) { - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - // Work around by temporarily setting element display to inline-block - return jQuery.swap( elem, { "display": "inline-block" }, - curCSS, [ elem, "marginRight" ] ); + return opt; +}; + +jQuery.fn.extend({ + fadeTo: function( speed, to, easing, callback ) { + + // show any hidden elements after setting opacity to 0 + return this.filter( isHidden ).css( "opacity", 0 ).show() + + // animate to the value specified + .end().animate({ opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + // Operate on a copy of prop so per-property easing won't be lost + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + // Empty animations, or finishing resolves immediately + if ( empty || data_priv.get( this, "finish" ) ) { + anim.stop( true ); } - } + }; + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); }; - } - // Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 - // getComputedStyle returns percent when specified for top/left/bottom/right - // rather than make the css module depend on the offset module, we just check for it here - if ( !jQuery.support.pixelPosition && jQuery.fn.position ) { - jQuery.each( [ "top", "left" ], function( i, prop ) { - jQuery.cssHooks[ prop ] = { - get: function( elem, computed ) { - if ( computed ) { - computed = curCSS( elem, prop ); - // if curCSS returns percentage, fallback to offset - return rnumnonpx.test( computed ) ? - jQuery( elem ).position()[ prop ] + "px" : - computed; + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue && type !== false ) { + this.queue( type || "fx", [] ); + } + + return this.each(function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = data_priv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); } } - }; + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + // start the next in the queue if the last step wasn't forced + // timers currently will call their complete callbacks, which will dequeue + // but only if they were gotoEnd + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } }); - } + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each(function() { + var index, + data = data_priv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + // enable finishing flag on private data + data.finish = true; + // empty the queue first + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + // look for any active animations, and finish them + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + // look for any animations in the old queue and finish them + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + // turn off finishing flag + delete data.finish; + }); + } }); -if ( jQuery.expr && jQuery.expr.filters ) { - jQuery.expr.filters.hidden = function( elem ) { - // Support: Opera <= 12.12 - // Opera reports offsetWidths and offsetHeights less than zero on some elements - return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 || - (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); +jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); }; +}); - jQuery.expr.filters.visible = function( elem ) { - return !jQuery.expr.filters.hidden( elem ); +// Generate shortcuts for custom animations +jQuery.each({ + slideDown: genFx("show"), + slideUp: genFx("hide"), + slideToggle: genFx("toggle"), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); }; -} +}); -// These hooks are used by animate to expand properties -jQuery.each({ - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; - // assumes a single number if not a string - parts = typeof value === "string" ? value.split(" ") : [ value ]; + fxNow = jQuery.now(); - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + // Checks the timer has not already been removed + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + if ( timer() ) { + jQuery.fx.start(); + } else { + jQuery.timers.pop(); + } +}; + +jQuery.fx.interval = 13; + +jQuery.fx.start = function() { + if ( !timerId ) { + timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); + } +}; + +jQuery.fx.stop = function() { + clearInterval( timerId ); + timerId = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + // Default speed + _default: 400 +}; + + +// Based off of the plugin by Clint Helfers, with permission. +// http://blindsignals.com/index.php/2009/07/jquery-delay/ +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); +}; + + +(function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + // Support: iOS 5.1, Android 4.x, Android 2.3 + // Check the default checkbox/radio value ("" on old WebKit; "on" elsewhere) + support.checkOn = input.value !== ""; + + // Must access the parent to make an option select properly + // Support: IE9, IE10 + support.optSelected = opt.selected; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Check if an input maintains its value after becoming a radio + // Support: IE9, IE10 + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +})(); + + +var nodeHook, boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend({ + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + } +}); + +jQuery.extend({ + attr: function( elem, name, value ) { + var hooks, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === strundefined ) { + return jQuery.prop( elem, name, value ); + } + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; } - return expanded; + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; } - }; + }, - if ( !rmargin.test( prefix ) ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( jQuery.expr.match.bool.test( name ) ) { + // Set corresponding property to false + elem[ propName ] = false; + } + + elem.removeAttribute( name ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + jQuery.nodeName( elem, "input" ) ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } } }); -var r20 = /%20/g, - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle; + if ( !isXML ) { + // Avoid an infinite loop by temporarily removing this function from the getter + handle = attrHandle[ name ]; + attrHandle[ name ] = ret; + ret = getter( elem, name, isXML ) != null ? + name.toLowerCase() : + null; + attrHandle[ name ] = handle; + } + return ret; + }; +}); + + + + +var rfocusable = /^(?:input|select|textarea|button)$/i; jQuery.fn.extend({ - serialize: function() { - return jQuery.param( this.serializeArray() ); + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); }, - serializeArray: function() { - return this.map(function(){ - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - }) - .filter(function(){ - var type = this.type; - // Use .is(":disabled") so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !manipulation_rcheckableType.test( type ) ); - }) - .map(function( i, elem ){ - var val = jQuery( this ).val(); - return val == null ? - null : - jQuery.isArray( val ) ? - jQuery.map( val, function( val ){ - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }) : - { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }).get(); + removeProp: function( name ) { + return this.each(function() { + delete this[ jQuery.propFix[ name ] || name ]; + }); } }); -//Serialize an array of form elements or a set of -//key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, value ) { - // If value is a function, invoke it and return its value - value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); - s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); - }; +jQuery.extend({ + propFix: { + "for": "htmlFor", + "class": "className" + }, - // Set traditional to true for jQuery <= 1.3.2 behavior. - if ( traditional === undefined ) { - traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? + ret : + ( elem[ name ] = value ); + + } else { + return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? + ret : + elem[ name ]; + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ? + elem.tabIndex : + -1; + } + } } +}); - // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); +// Support: IE9+ +// Selectedness for an option in an optgroup can be inaccurate +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + } + }; +} + +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); + + + + +var rclass = /[\t\r\n\f]/g; + +jQuery.fn.extend({ + addClass: function( value ) { + var classes, elem, cur, clazz, j, finalValue, + proceed = typeof value === "string" && value, + i = 0, + len = this.length; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + + // only assign if different to avoid unneeded rendering. + finalValue = jQuery.trim( cur ); + if ( elem.className !== finalValue ) { + elem.className = finalValue; + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, finalValue, + proceed = arguments.length === 0 || typeof value === "string" && value, + i = 0, + len = this.length; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + + // only assign if different to avoid unneeded rendering. + finalValue = value ? jQuery.trim( cur ) : ""; + if ( elem.className !== finalValue ) { + elem.className = finalValue; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value; + + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + classNames = value.match( rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( type === strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + data_priv.set( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : data_priv.get( this, "__className__" ) || ""; + } }); + }, - } else { - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } } + + return false; } +}); - // Return the resulting serialization - return s.join( "&" ).replace( r20, "+" ); -}; -function buildParams( prefix, obj, traditional, add ) { - var name; - if ( jQuery.isArray( obj ) ) { - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - // Treat each array item as a scalar. - add( prefix, v ); +var rreturn = /\r/g; + +jQuery.fn.extend({ + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); } else { - // Item is non-scalar (array or object), encode its numeric index. - buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; } }); + } +}); - } else if ( !traditional && jQuery.type( obj ) === "object" ) { - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + // Support: IE10-11+ + // option.text throws exceptions (#14686, #14858) + jQuery.trim( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // IE6-9 doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( support.optDisabled ? !option.disabled : option.getAttribute( "disabled" ) === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + if ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) { + optionSet = true; + } + } + + // force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } } + } +}); - } else { - // Serialize scalar item. - add( prefix, obj ); +// Radios and checkboxes getter/setter +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; } -} +}); + + + + +// Return jQuery for attributes-only inclusion + + jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { @@ -7584,24 +7482,56 @@ jQuery.fn.extend({ return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); } }); + + +var nonce = jQuery.now(); + +var rquery = (/\?/); + + + +// Support: Android 2.3 +// Workaround failure to string-cast null input +jQuery.parseJSON = function( data ) { + return JSON.parse( data + "" ); +}; + + +// Cross-browser xml parsing +jQuery.parseXML = function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + + // Support: IE9 + try { + tmp = new DOMParser(); + xml = tmp.parseFromString( data, "text/xml" ); + } catch ( e ) { + xml = undefined; + } + + if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; +}; + + var // Document location ajaxLocParts, ajaxLocation, - ajax_nonce = jQuery.now(), - ajax_rquery = /\?/, rhash = /#.*$/, rts = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, // #7653, #8125, #8152: local protocol detection rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, rnoContent = /^(?:GET|HEAD)$/, rprotocol = /^\/\//, - rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/, - - // Keep a copy of the old load method - _load = jQuery.fn.load, + rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/, /* Prefilters * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) @@ -7652,7 +7582,7 @@ function addToPrefiltersOrTransports( structure ) { var dataType, i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( core_rnotwhite ) || []; + dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; if ( jQuery.isFunction( func ) ) { // For each dataType in the dataTypeExpression @@ -7682,7 +7612,7 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jqX inspected[ dataType ] = true; jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { options.dataTypes.unshift( dataTypeOrTransport ); inspect( dataTypeOrTransport ); return false; @@ -7700,7 +7630,7 @@ function inspectPrefiltersOrTransports( structure, options, originalOptions, jqX // that takes "flat" options (not to be deep extended) // Fixes #9887 function ajaxExtend( target, src ) { - var deep, key, + var key, deep, flatOptions = jQuery.ajaxSettings.flatOptions || {}; for ( key in src ) { @@ -7715,69 +7645,156 @@ function ajaxExtend( target, src ) { return target; } -jQuery.fn.load = function( url, params, callback ) { - if ( typeof url !== "string" && _load ) { - return _load.apply( this, arguments ); +/* Handles responses to an ajax request: + * - finds the right dataType (mediates between content-type and expected dataType) + * - returns the corresponding response + */ +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + // Remove auto dataType and get content-type in the process + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); + } } - var selector, response, type, - self = this, - off = url.indexOf(" "); + // Check if we're dealing with a known content-type + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } - if ( off >= 0 ) { - selector = url.slice( off, url.length ); - url = url.slice( 0, off ); + // Check to see if we have a response for the expected dataType + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + // Try convertible dataTypes + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + // Or just use first one + finalDataType = finalDataType || firstDataType; } - // If it's a function - if ( jQuery.isFunction( params ) ) { + // If we found a dataType + // We add the dataType to the list if needed + // and return the corresponding response + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} - // We assume that it's the callback - callback = params; - params = undefined; +/* Chain conversions given the request and the original response + * Also sets the responseXXX fields on the jqXHR instance + */ +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + // Work with a copy of dataTypes in case we need to modify it for conversion + dataTypes = s.dataTypes.slice(); - // Otherwise, build a param string - } else if ( params && typeof params === "object" ) { - type = "POST"; + // Create converters map with lowercased keys + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } } - // If we have elements to modify, make the request - if ( self.length > 0 ) { - jQuery.ajax({ - url: url, + current = dataTypes.shift(); - // if "type" variable is undefined, then "GET" method will be used - type: type, - dataType: "html", - data: params - }).done(function( responseText ) { + // Convert to each sequential dataType + while ( current ) { - // Save response for use in complete callback - response = arguments; + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } - self.html( selector ? + // Apply the dataFilter if provided + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } - // If a selector was specified, locate the right elements in a dummy div - // Exclude scripts to avoid IE 'Permission Denied' errors - jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : + prev = current; + current = dataTypes.shift(); - // Otherwise use the full result - responseText ); + if ( current ) { - }).complete( callback && function( jqXHR, status ) { - self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); - }); - } + // There's only work to do if current dataType is non-auto + if ( current === "*" ) { - return this; -}; + current = prev; -// Attach a bunch of functions for handling common AJAX events -jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){ - jQuery.fn[ type ] = function( fn ){ - return this.on( type, fn ); - }; -}); + // Convert response if prev dataType is non-auto and differs from current + } else if ( prev !== "*" && prev !== current ) { + + // Seek a direct converter + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + // If none found, seek a pair + if ( !conv ) { + for ( conv2 in converters ) { + + // If conv2 outputs current + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + // If prev can be converted to accepted input + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + // Condense equivalence converters + if ( conv === true ) { + conv = converters[ conv2 ]; + + // Otherwise, insert the intermediate dataType + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + // Apply converter (if not an equivalence) + if ( conv !== true ) { + + // Unless errors are allowed to bubble, catch and return them + if ( conv && s[ "throws" ] ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} jQuery.extend({ @@ -7883,23 +7900,20 @@ jQuery.extend({ // Force options to be an object options = options || {}; - var // Cross-domain detection vars - parts, - // Loop variable - i, + var transport, // URL without anti-cache param cacheURL, - // Response headers as string + // Response headers responseHeadersString, + responseHeaders, // timeout handle timeoutTimer, - + // Cross-domain detection vars + parts, // To know if global events are to be dispatched fireGlobals, - - transport, - // Response headers - responseHeaders, + // Loop variable + i, // Create the final options object s = jQuery.ajaxSetup( {}, options ), // Callbacks context @@ -7996,16 +8010,17 @@ jQuery.extend({ jqXHR.error = jqXHR.fail; // Remove hash character (#7531: and string promotion) - // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) + // Add protocol if not provided (prefilters might expect it) // Handle falsy url in the settings object (#10093: consistency with old signature) // We also use the url parameter if available - s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); + s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ) + .replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); // Alias method option to type as per ticket #12004 s.type = options.method || options.type || s.method || s.type; // Extract dataTypes list - s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""]; + s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; // A cross-domain request is in order when we have a protocol:host:port mismatch if ( s.crossDomain == null ) { @@ -8053,7 +8068,7 @@ jQuery.extend({ // If data is available, append data to url if ( s.data ) { - cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); + cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); // #9682: remove data so that it's not used in an eventual retry delete s.data; } @@ -8063,10 +8078,10 @@ jQuery.extend({ s.url = rts.test( cacheURL ) ? // If there is already a '_' parameter, set its value - cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) : + cacheURL.replace( rts, "$1_=" + nonce++ ) : // Otherwise add one to the end - cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++; + cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++; } } @@ -8290,155 +8305,342 @@ jQuery.each( [ "get", "post" ], function( i, method ) { }; }); -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - var firstDataType, ct, finalDataType, type, - contents = s.contents, - dataTypes = s.dataTypes; +// Attach a bunch of functions for handling common AJAX events +jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) { + jQuery.fn[ type ] = function( fn ) { + return this.on( type, fn ); + }; +}); - // Remove auto dataType and get content-type in the process - while( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); + +jQuery._evalUrl = function( url ) { + return jQuery.ajax({ + url: url, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); +}; + + +jQuery.fn.extend({ + wrapAll: function( html ) { + var wrap; + + if ( jQuery.isFunction( html ) ) { + return this.each(function( i ) { + jQuery( this ).wrapAll( html.call(this, i) ); + }); } - } - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; + if ( this[ 0 ] ) { + + // The elements to wrap the target around + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); } + + wrap.map(function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + }).append( this ); } - } - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { - finalDataType = type; - break; + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function( i ) { + jQuery( this ).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); } - if ( !firstDataType ) { - firstDataType = type; + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function( i ) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); } - } - // Or just use first one - finalDataType = finalDataType || firstDataType; + }).end(); } +}); - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); + +jQuery.expr.filters.hidden = function( elem ) { + // Support: Opera <= 12.12 + // Opera reports offsetWidths and offsetHeights less than zero on some elements + return elem.offsetWidth <= 0 && elem.offsetHeight <= 0; +}; +jQuery.expr.filters.visible = function( elem ) { + return !jQuery.expr.filters.hidden( elem ); +}; + + + + +var r20 = /%20/g, + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( jQuery.isArray( obj ) ) { + // Serialize array item. + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + // Treat each array item as a scalar. + add( prefix, v ); + + } else { + // Item is non-scalar (array or object), encode its numeric index. + buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); + } + }); + + } else if ( !traditional && jQuery.type( obj ) === "object" ) { + // Serialize object item. + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); } - return responses[ finalDataType ]; + + } else { + // Serialize scalar item. + add( prefix, obj ); } } -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); +// Serialize an array of form elements or a set of +// key/values into a query string +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, value ) { + // If value is a function, invoke it and return its value + value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); + s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); + }; - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } + // Set traditional to true for jQuery <= 1.3.2 behavior. + if ( traditional === undefined ) { + traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; } - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { + // If an array was passed in, assume that it is an array of form elements. + if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + // Serialize the form elements + jQuery.each( a, function() { + add( this.name, this.value ); + }); - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; + } else { + // If traditional, encode the "old" way (the way 1.3.2 or older + // did it), otherwise encode params recursively. + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); } + } - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } + // Return the resulting serialization + return s.join( "&" ).replace( r20, "+" ); +}; - prev = current; - current = dataTypes.shift(); +jQuery.fn.extend({ + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map(function() { + // Can add propHook for "elements" to filter or add form elements + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + }) + .filter(function() { + var type = this.type; - if ( current ) { + // Use .is( ":disabled" ) so that fieldset[disabled] works + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + }) + .map(function( i, elem ) { + var val = jQuery( this ).val(); - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { + return val == null ? + null : + jQuery.isArray( val ) ? + jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }) : + { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + }).get(); + } +}); - current = prev; - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { +jQuery.ajaxSettings.xhr = function() { + try { + return new XMLHttpRequest(); + } catch( e ) {} +}; - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; +var xhrId = 0, + xhrCallbacks = {}, + xhrSuccessStatus = { + // file protocol always yields status code 0, assume 200 + 0: 200, + // Support: IE9 + // #1450: sometimes IE returns 1223 when it should be 204 + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { +// Support: IE9 +// Open requests must be manually aborted on unload (#5280) +if ( window.ActiveXObject ) { + jQuery( window ).on( "unload", function() { + for ( var key in xhrCallbacks ) { + xhrCallbacks[ key ](); + } + }); +} - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; +jQuery.ajaxTransport(function( options ) { + var callback; - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } + // Cross domain only allowed if supported through XMLHttpRequest + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(), + id = ++xhrId; + + xhr.open( options.type, options.url, options.async, options.username, options.password ); + + // Apply custom fields if provided + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; } } - // Apply converter (if not an equivalence) - if ( conv !== true ) { + // Override mime type if needed + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } - // Unless errors are allowed to bubble, catch and return them - if ( conv && s[ "throws" ] ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; + // X-Requested-With header + // For cross-domain requests, seeing as conditions for a preflight are + // akin to a jigsaw puzzle, we simply never set it to be sure. + // (it can always be set on a per-request basis or even using ajaxSetup) + // For same-domain requests, won't change header if already provided. + if ( !options.crossDomain && !headers["X-Requested-With"] ) { + headers["X-Requested-With"] = "XMLHttpRequest"; + } + + // Set headers + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + + // Callback + callback = function( type ) { + return function() { + if ( callback ) { + delete xhrCallbacks[ id ]; + callback = xhr.onload = xhr.onerror = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + complete( + // file: protocol always yields status 0; see #8605, #14207 + xhr.status, + xhr.statusText + ); + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + // Support: IE9 + // Accessing binary-data responseText throws an exception + // (#11426) + typeof xhr.responseText === "string" ? { + text: xhr.responseText + } : undefined, + xhr.getAllResponseHeaders() + ); + } } + }; + }; + + // Listen to events + xhr.onload = callback(); + xhr.onerror = callback("error"); + + // Create the abort callback + callback = xhrCallbacks[ id ] = callback("abort"); + + try { + // Do send the request (this may raise an exception) + xhr.send( options.hasContent && options.data || null ); + } catch ( e ) { + // #14683: Only rethrow if this hasn't been notified as an error yet + if ( callback ) { + throw e; } } + }, + + abort: function() { + if ( callback ) { + callback(); + } } - } + }; } +}); + + + - return { state: "success", data: response }; -} // Install script dataType jQuery.ajaxSetup({ accepts: { @@ -8455,76 +8657,51 @@ jQuery.ajaxSetup({ } }); -// Handle cache's special case and global +// Handle cache's special case and crossDomain jQuery.ajaxPrefilter( "script", function( s ) { if ( s.cache === undefined ) { s.cache = false; } if ( s.crossDomain ) { s.type = "GET"; - s.global = false; } }); // Bind script tag hack transport -jQuery.ajaxTransport( "script", function(s) { - +jQuery.ajaxTransport( "script", function( s ) { // This transport only deals with cross domain requests if ( s.crossDomain ) { - - var script, - head = document.head || jQuery("head")[0] || document.documentElement; - + var script, callback; return { - - send: function( _, callback ) { - - script = document.createElement("script"); - - script.async = true; - - if ( s.scriptCharset ) { - script.charset = s.scriptCharset; - } - - script.src = s.url; - - // Attach handlers for all browsers - script.onload = script.onreadystatechange = function( _, isAbort ) { - - if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { - - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - - // Remove the script - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = null; - - // Callback if not abort - if ( !isAbort ) { - callback( 200, "success" ); + send: function( _, complete ) { + script = jQuery("<script>").prop({ + async: true, + charset: s.scriptCharset, + src: s.url + }).on( + "load error", + callback = function( evt ) { + script.remove(); + callback = null; + if ( evt ) { + complete( evt.type === "error" ? 404 : 200, evt.type ); } } - }; - - // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending - // Use native DOM manipulation to avoid our domManip AJAX trickery - head.insertBefore( script, head.firstChild ); + ); + document.head.appendChild( script[ 0 ] ); }, - abort: function() { - if ( script ) { - script.onload( undefined, true ); + if ( callback ) { + callback(); } } }; } }); + + + + var oldCallbacks = [], rjsonp = /(=)\?(?=&|$)|\?\?/; @@ -8532,7 +8709,7 @@ var oldCallbacks = [], jQuery.ajaxSetup({ jsonp: "callback", jsonpCallback: function() { - var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) ); + var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); this[ callback ] = true; return callback; } @@ -8559,7 +8736,7 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { if ( jsonProp ) { s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); } else if ( s.jsonp !== false ) { - s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; } // Use data converter to retrieve json after script execution @@ -8605,1004 +8782,149 @@ jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { return "script"; } }); -var xhrCallbacks, xhrSupported, - xhrId = 0, - // #5280: Internet Explorer will keep connections alive if we don't abort on unload - xhrOnUnloadAbort = window.ActiveXObject && function() { - // Abort all pending requests - var key; - for ( key in xhrCallbacks ) { - xhrCallbacks[ key ]( undefined, true ); - } - }; - -// Functions to create xhrs -function createStandardXHR() { - try { - return new window.XMLHttpRequest(); - } catch( e ) {} -} - -function createActiveXHR() { - try { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } catch( e ) {} -} - -// Create the request object -// (This is still attached to ajaxSettings for backward compatibility) -jQuery.ajaxSettings.xhr = window.ActiveXObject ? - /* Microsoft failed to properly - * implement the XMLHttpRequest in IE7 (can't request local files), - * so we use the ActiveXObject when it is available - * Additionally XMLHttpRequest can be disabled in IE7/IE8 so - * we need a fallback. - */ - function() { - return !this.isLocal && createStandardXHR() || createActiveXHR(); - } : - // For all other browsers, use the standard XMLHttpRequest object - createStandardXHR; - -// Determine support properties -xhrSupported = jQuery.ajaxSettings.xhr(); -jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -xhrSupported = jQuery.support.ajax = !!xhrSupported; - -// Create transport if the browser can provide an xhr -if ( xhrSupported ) { - - jQuery.ajaxTransport(function( s ) { - // Cross domain only allowed if supported through XMLHttpRequest - if ( !s.crossDomain || jQuery.support.cors ) { - - var callback; - - return { - send: function( headers, complete ) { - - // Get a new xhr - var handle, i, - xhr = s.xhr(); - - // Open the socket - // Passing null username, generates a login popup on Opera (#2865) - if ( s.username ) { - xhr.open( s.type, s.url, s.async, s.username, s.password ); - } else { - xhr.open( s.type, s.url, s.async ); - } - - // Apply custom fields if provided - if ( s.xhrFields ) { - for ( i in s.xhrFields ) { - xhr[ i ] = s.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( s.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( s.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !s.crossDomain && !headers["X-Requested-With"] ) { - headers["X-Requested-With"] = "XMLHttpRequest"; - } - - // Need an extra try/catch for cross domain requests in Firefox 3 - try { - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - } catch( err ) {} - - // Do send the request - // This may raise an exception which is actually - // handled in jQuery.ajax (so no try/catch here) - xhr.send( ( s.hasContent && s.data ) || null ); - - // Listener - callback = function( _, isAbort ) { - var status, responseHeaders, statusText, responses; - - // Firefox throws exceptions when accessing properties - // of an xhr when a network error occurred - // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE) - try { - - // Was never called and is aborted or complete - if ( callback && ( isAbort || xhr.readyState === 4 ) ) { - - // Only called once - callback = undefined; - - // Do not keep as active anymore - if ( handle ) { - xhr.onreadystatechange = jQuery.noop; - if ( xhrOnUnloadAbort ) { - delete xhrCallbacks[ handle ]; - } - } - - // If it's an abort - if ( isAbort ) { - // Abort it manually if needed - if ( xhr.readyState !== 4 ) { - xhr.abort(); - } - } else { - responses = {}; - status = xhr.status; - responseHeaders = xhr.getAllResponseHeaders(); - - // When requesting binary data, IE6-9 will throw an exception - // on any attempt to access responseText (#11426) - if ( typeof xhr.responseText === "string" ) { - responses.text = xhr.responseText; - } - - // Firefox throws an exception when accessing - // statusText for faulty cross-domain requests - try { - statusText = xhr.statusText; - } catch( e ) { - // We normalize with Webkit giving an empty statusText - statusText = ""; - } - - // Filter status for non standard behaviors - - // If the request is local and we have data: assume a success - // (success with no data won't get notified, that's the best we - // can do given current implementations) - if ( !status && s.isLocal && !s.crossDomain ) { - status = responses.text ? 200 : 404; - // IE - #1450: sometimes returns 1223 when it should be 204 - } else if ( status === 1223 ) { - status = 204; - } - } - } - } catch( firefoxAccessException ) { - if ( !isAbort ) { - complete( -1, firefoxAccessException ); - } - } - - // Call complete if needed - if ( responses ) { - complete( status, statusText, responses, responseHeaders ); - } - }; - - if ( !s.async ) { - // if we're in sync mode we fire the callback - callback(); - } else if ( xhr.readyState === 4 ) { - // (IE6 & IE7) if it's in cache and has been - // retrieved directly we need to fire the callback - setTimeout( callback ); - } else { - handle = ++xhrId; - if ( xhrOnUnloadAbort ) { - // Create the active xhrs callbacks list if needed - // and attach the unload handler - if ( !xhrCallbacks ) { - xhrCallbacks = {}; - jQuery( window ).unload( xhrOnUnloadAbort ); - } - // Add to list of active xhrs callbacks - xhrCallbacks[ handle ] = callback; - } - xhr.onreadystatechange = callback; - } - }, - - abort: function() { - if ( callback ) { - callback( undefined, true ); - } - } - }; - } - }); -} -var fxNow, timerId, - rfxtypes = /^(?:toggle|show|hide)$/, - rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ), - rrun = /queueHooks$/, - animationPrefilters = [ defaultPrefilter ], - tweeners = { - "*": [function( prop, value ) { - var tween = this.createTween( prop, value ), - target = tween.cur(), - parts = rfxnum.exec( value ), - unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && - rfxnum.exec( jQuery.css( tween.elem, prop ) ), - scale = 1, - maxIterations = 20; - - if ( start && start[ 3 ] !== unit ) { - // Trust units reported by jQuery.css - unit = unit || start[ 3 ]; - - // Make sure we update the tween properties later on - parts = parts || []; - - // Iteratively approximate from a nonzero starting point - start = +target || 1; - - do { - // If previous iteration zeroed out, double until we get *something* - // Use a string for doubling factor so we don't accidentally see scale as unchanged below - scale = scale || ".5"; - - // Adjust and apply - start = start / scale; - jQuery.style( tween.elem, prop, start + unit ); - - // Update scale, tolerating zero or NaN from tween.cur() - // And breaking the loop if scale is unchanged or perfect, or if we've just had enough - } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); - } - - // Update tween properties - if ( parts ) { - start = tween.start = +start || +target || 0; - tween.unit = unit; - // If a +=/-= token was provided, we're doing a relative animation - tween.end = parts[ 1 ] ? - start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : - +parts[ 2 ]; - } - - return tween; - }] - }; - -// Animations created synchronously will run synchronously -function createFxNow() { - setTimeout(function() { - fxNow = undefined; - }); - return ( fxNow = jQuery.now() ); -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( (tween = collection[ index ].call( animation, prop, value )) ) { - - // we're done with this property - return tween; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = animationPrefilters.length, - deferred = jQuery.Deferred().always( function() { - // don't match elem in the :animated selector - delete tick.elem; - }), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ]); - - if ( percent < 1 && length ) { - return remaining; - } else { - deferred.resolveWith( elem, [ animation ] ); - return false; - } - }, - animation = deferred.promise({ - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { specialEasing: {} }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - // if we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // resolve when we played the last frame - // otherwise, reject - if ( gotoEnd ) { - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - }), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length ; index++ ) { - result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( jQuery.isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - }) - ); - - // attach callbacks from options - return animation.progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = jQuery.camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( jQuery.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // not quite $.extend, this wont overwrite keys already present. - // also - reusing 'index' from above because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.split(" "); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length ; index++ ) { - prop = props[ index ]; - tweeners[ prop ] = tweeners[ prop ] || []; - tweeners[ prop ].unshift( callback ); - } - }, - - prefilter: function( callback, prepend ) { - if ( prepend ) { - animationPrefilters.unshift( callback ); - } else { - animationPrefilters.push( callback ); - } - } -}); -function defaultPrefilter( elem, props, opts ) { - /* jshint validthis: true */ - var prop, value, toggle, tween, hooks, oldfire, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHidden( elem ), - dataShow = jQuery._data( elem, "fxshow" ); - // handle queue: false promises - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - anim.always(function() { - // doing this makes sure that the complete handler will be called - // before this completes - anim.always(function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - }); - }); - } - - // height/width overflow pass - if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { - // Make sure that nothing sneaks out - // Record all 3 overflow attributes because IE does not - // change the overflow attribute when overflowX and - // overflowY are set to the same value - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Set display property to inline-block for height/width - // animations on inline elements that are having width/height animated - if ( jQuery.css( elem, "display" ) === "inline" && - jQuery.css( elem, "float" ) === "none" ) { - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) { - style.display = "inline-block"; - - } else { - style.zoom = 1; - } - } +// data: string of html +// context (optional): If specified, the fragment will be created in this context, defaults to document +// keepScripts (optional): If true, will include scripts passed in the html string +jQuery.parseHTML = function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { + return null; } - - if ( opts.overflow ) { - style.overflow = "hidden"; - if ( !jQuery.support.shrinkWrapBlocks ) { - anim.always(function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - }); - } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; } + context = context || document; + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; - // show/hide pass - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.exec( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - continue; - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; } - if ( !jQuery.isEmptyObject( orig ) ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = jQuery._data( elem, "fxshow", {} ); - } + parsed = jQuery.buildFragment( [ data ], context, scripts ); - // store state if its toggle - enables .stop().toggle() to "reverse" - if ( toggle ) { - dataShow.hidden = !hidden; - } - if ( hidden ) { - jQuery( elem ).show(); - } else { - anim.done(function() { - jQuery( elem ).hide(); - }); - } - anim.done(function() { - var prop; - jQuery._removeData( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - }); - for ( prop in orig ) { - tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = tween.start; - if ( hidden ) { - tween.end = tween.start; - tween.start = prop === "width" || prop === "height" ? 1 : 0; - } - } - } + if ( scripts && scripts.length ) { + jQuery( scripts ).remove(); } -} - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || "swing"; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } + return jQuery.merge( [], parsed.childNodes ); }; -Tween.prototype.init.prototype = Tween.prototype; -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; +// Keep a copy of the old load method +var _load = jQuery.fn.load; - if ( tween.elem[ tween.prop ] != null && - (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { - return tween.elem[ tween.prop ]; - } - - // passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails - // so, simple values such as "10px" are parsed to Float. - // complex values such as "rotate(1rad)" are returned as is. - result = jQuery.css( tween.elem, tween.prop, "" ); - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - // use step hook for back compat - use cssHook if its there - use .style if its - // available and use plain properties where available - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } +/** + * Load a url into a page + */ +jQuery.fn.load = function( url, params, callback ) { + if ( typeof url !== "string" && _load ) { + return _load.apply( this, arguments ); } -}; -// Support: IE <=9 -// Panic based approach to setting things on disconnected nodes + var selector, type, response, + self = this, + off = url.indexOf(" "); -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } + if ( off >= 0 ) { + selector = jQuery.trim( url.slice( off ) ); + url = url.slice( 0, off ); } -}; - -jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -}); - -jQuery.fn.extend({ - fadeTo: function( speed, to, easing, callback ) { - - // show any hidden elements after setting opacity to 0 - return this.filter( isHidden ).css( "opacity", 0 ).show() - - // animate to the value specified - .end().animate({ opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || jQuery._data( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue && type !== false ) { - this.queue( type || "fx", [] ); - } - return this.each(function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = jQuery._data( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // start the next in the queue if the last step wasn't forced - // timers currently will call their complete callbacks, which will dequeue - // but only if they were gotoEnd - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - }); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each(function() { - var index, - data = jQuery._data( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // enable finishing flag on private data - data.finish = true; - - // empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } + // If it's a function + if ( jQuery.isFunction( params ) ) { - // look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } + // We assume that it's the callback + callback = params; + params = undefined; - // turn off finishing flag - delete data.finish; - }); + // Otherwise, build a param string + } else if ( params && typeof params === "object" ) { + type = "POST"; } -}); - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - attrs = { height: type }, - i = 0; - // if we include width, step value is 1 to do all cssExpand values, - // if we don't include width, step value is 2 to skip over Left and Right - includeWidth = includeWidth? 1 : 0; - for( ; i < 4 ; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } + // If we have elements to modify, make the request + if ( self.length > 0 ) { + jQuery.ajax({ + url: url, - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } + // if "type" variable is undefined, then "GET" method will be used + type: type, + dataType: "html", + data: params + }).done(function( responseText ) { - return attrs; -} + // Save response for use in complete callback + response = arguments; -// Generate shortcuts for custom animations -jQuery.each({ - slideDown: genFx("show"), - slideUp: genFx("hide"), - slideToggle: genFx("toggle"), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -}); + self.html( selector ? -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing - }; + // If a selector was specified, locate the right elements in a dummy div + // Exclude scripts to avoid IE 'Permission Denied' errors + jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : - opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; + // Otherwise use the full result + responseText ); - // normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; + }).complete( callback && function( jqXHR, status ) { + self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); + }); } - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p*Math.PI ) / 2; - } + return this; }; -jQuery.timers = []; -jQuery.fx = Tween.prototype.init; -jQuery.fx.tick = function() { - var timer, - timers = jQuery.timers, - i = 0; - fxNow = jQuery.now(); - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - // Checks the timer has not already been removed - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; +jQuery.expr.filters.animated = function( elem ) { + return jQuery.grep(jQuery.timers, function( fn ) { + return elem === fn.elem; + }).length; }; -jQuery.fx.timer = function( timer ) { - if ( timer() && jQuery.timers.push( timer ) ) { - jQuery.fx.start(); - } -}; -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( !timerId ) { - timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); - } -}; -jQuery.fx.stop = function() { - clearInterval( timerId ); - timerId = null; -}; +var docElem = window.document.documentElement; -jQuery.fx.speeds = { - slow: 600, - fast: 200, - // Default speed - _default: 400 -}; - -// Back Compat <1.8 extension point -jQuery.fx.step = {}; - -if ( jQuery.expr && jQuery.expr.filters ) { - jQuery.expr.filters.animated = function( elem ) { - return jQuery.grep(jQuery.timers, function( fn ) { - return elem === fn.elem; - }).length; - }; +/** + * Gets a window from an element + */ +function getWindow( elem ) { + return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView; } -jQuery.fn.offset = function( options ) { - if ( arguments.length ) { - return options === undefined ? - this : - this.each(function( i ) { - jQuery.offset.setOffset( this, options, i ); - }); - } - - var docElem, win, - box = { top: 0, left: 0 }, - elem = this[ 0 ], - doc = elem && elem.ownerDocument; - - if ( !doc ) { - return; - } - - docElem = doc.documentElement; - - // Make sure it's not a disconnected DOM node - if ( !jQuery.contains( docElem, elem ) ) { - return box; - } - - // If we don't have gBCR, just use 0,0 rather than error - // BlackBerry 5, iOS 3 (original iPhone) - if ( typeof elem.getBoundingClientRect !== core_strundefined ) { - box = elem.getBoundingClientRect(); - } - win = getWindow( doc ); - return { - top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), - left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) - }; -}; jQuery.offset = { - setOffset: function( elem, options, i ) { - var position = jQuery.css( elem, "position" ); + var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, + position = jQuery.css( elem, "position" ), + curElem = jQuery( elem ), + props = {}; - // set position first, in-case top/left are set even on static elem + // Set position first, in-case top/left are set even on static elem if ( position === "static" ) { elem.style.position = "relative"; } - var curElem = jQuery( elem ), - curOffset = curElem.offset(), - curCSSTop = jQuery.css( elem, "top" ), - curCSSLeft = jQuery.css( elem, "left" ), - calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1, - props = {}, curPosition = {}, curTop, curLeft; + curOffset = curElem.offset(); + curCSSTop = jQuery.css( elem, "top" ); + curCSSLeft = jQuery.css( elem, "left" ); + calculatePosition = ( position === "absolute" || position === "fixed" ) && + ( curCSSTop + curCSSLeft ).indexOf("auto") > -1; - // need to be able to calculate position if either top or left is auto and position is either absolute or fixed + // Need to be able to calculate position if either top or left is auto and position is either absolute or fixed if ( calculatePosition ) { curPosition = curElem.position(); curTop = curPosition.top; curLeft = curPosition.left; + } else { curTop = parseFloat( curCSSTop ) || 0; curLeft = parseFloat( curCSSLeft ) || 0; @@ -9621,14 +8943,50 @@ jQuery.offset = { if ( "using" in options ) { options.using.call( elem, props ); + } else { curElem.css( props ); } } }; - jQuery.fn.extend({ + offset: function( options ) { + if ( arguments.length ) { + return options === undefined ? + this : + this.each(function( i ) { + jQuery.offset.setOffset( this, options, i ); + }); + } + + var docElem, win, + elem = this[ 0 ], + box = { top: 0, left: 0 }, + doc = elem && elem.ownerDocument; + + if ( !doc ) { + return; + } + + docElem = doc.documentElement; + + // Make sure it's not a disconnected DOM node + if ( !jQuery.contains( docElem, elem ) ) { + return box; + } + + // If we don't have gBCR, just use 0,0 rather than error + // BlackBerry 5, iOS 3 (original iPhone) + if ( typeof elem.getBoundingClientRect !== strundefined ) { + box = elem.getBoundingClientRect(); + } + win = getWindow( doc ); + return { + top: box.top + win.pageYOffset - docElem.clientTop, + left: box.left + win.pageXOffset - docElem.clientLeft + }; + }, position: function() { if ( !this[ 0 ] ) { @@ -9636,13 +8994,14 @@ jQuery.fn.extend({ } var offsetParent, offset, - parentOffset = { top: 0, left: 0 }, - elem = this[ 0 ]; + elem = this[ 0 ], + parentOffset = { top: 0, left: 0 }; - // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent + // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent if ( jQuery.css( elem, "position" ) === "fixed" ) { - // we assume that getBoundingClientRect is available when computed position is fixed + // We assume that getBoundingClientRect is available when computed position is fixed offset = elem.getBoundingClientRect(); + } else { // Get *real* offsetParent offsetParent = this.offsetParent(); @@ -9654,49 +9013,46 @@ jQuery.fn.extend({ } // Add offsetParent borders - parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); + parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); } // Subtract parent offsets and element margins - // note: when an element has margin: auto the offsetLeft and marginLeft - // are the same in Safari causing offset.left to incorrectly be 0 return { - top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), - left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true) + top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), + left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true ) }; }, offsetParent: function() { return this.map(function() { var offsetParent = this.offsetParent || docElem; - while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) { + + while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { offsetParent = offsetParent.offsetParent; } + return offsetParent || docElem; }); } }); - // Create scrollLeft and scrollTop methods -jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) { - var top = /Y/.test( prop ); +jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { + var top = "pageYOffset" === prop; jQuery.fn[ method ] = function( val ) { - return jQuery.access( this, function( elem, method, val ) { + return access( this, function( elem, method, val ) { var win = getWindow( elem ); if ( val === undefined ) { - return win ? (prop in win) ? win[ prop ] : - win.document.documentElement[ method ] : - elem[ method ]; + return win ? win[ prop ] : elem[ method ]; } if ( win ) { win.scrollTo( - !top ? val : jQuery( win ).scrollLeft(), - top ? val : jQuery( win ).scrollTop() + !top ? val : window.pageXOffset, + top ? val : window.pageYOffset ); } else { @@ -9706,13 +9062,25 @@ jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( me }; }); -function getWindow( elem ) { - return jQuery.isWindow( elem ) ? - elem : - elem.nodeType === 9 ? - elem.defaultView || elem.parentWindow : - false; -} +// Add the top/left cssHooks using jQuery.fn.position +// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 +// getComputedStyle returns percent when specified for top/left/bottom/right +// rather than make the css module depend on the offset module, we just check for it here +jQuery.each( [ "top", "left" ], function( i, prop ) { + jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, + function( elem, computed ) { + if ( computed ) { + computed = curCSS( elem, prop ); + // if curCSS returns percentage, fallback to offset + return rnumnonpx.test( computed ) ? + jQuery( elem ).position()[ prop ] + "px" : + computed; + } + } + ); +}); + + // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { @@ -9721,7 +9089,7 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); - return jQuery.access( this, function( elem, type, value ) { + return access( this, function( elem, type, value ) { var doc; if ( jQuery.isWindow( elem ) ) { @@ -9735,8 +9103,8 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { if ( elem.nodeType === 9 ) { doc = elem.documentElement; - // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest - // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. + // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], + // whichever is greatest return Math.max( elem.body[ "scroll" + name ], doc[ "scroll" + name ], elem.body[ "offset" + name ], doc[ "offset" + name ], @@ -9754,8 +9122,7 @@ jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { }; }); }); -// Limit scope pollution from any deprecated API -// (function() { + // The number of elements contained in the matched element set jQuery.fn.size = function() { @@ -9764,33 +9131,66 @@ jQuery.fn.size = function() { jQuery.fn.andSelf = jQuery.fn.addBack; -// })(); -if ( typeof module === "object" && module && typeof module.exports === "object" ) { - // Expose jQuery as module.exports in loaders that implement the Node - // module pattern (including browserify). Do not create the global, since - // the user will be storing it themselves locally, and globals are frowned - // upon in the Node module world. - module.exports = jQuery; -} else { - // Otherwise expose jQuery to the global object as usual - window.jQuery = window.$ = jQuery; - // Register as a named AMD module, since jQuery can be concatenated with other - // files that may use define, but not via a proper concatenation script that - // understands anonymous AMD modules. A named AMD is safest and most robust - // way to register. Lowercase jquery is used because AMD module names are - // derived from file names, and jQuery is normally delivered in a lowercase - // file name. Do this after creating the global so that if an AMD module wants - // to call noConflict to hide this version of jQuery, it will work. - if ( typeof define === "function" && define.amd ) { - define( "jquery", [], function () { return jQuery; } ); + + +// Register as a named AMD module, since jQuery can be concatenated with other +// files that may use define, but not via a proper concatenation script that +// understands anonymous AMD modules. A named AMD is safest and most robust +// way to register. Lowercase jquery is used because AMD module names are +// derived from file names, and jQuery is normally delivered in a lowercase +// file name. Do this after creating the global so that if an AMD module wants +// to call noConflict to hide this version of jQuery, it will work. + +// Note that for maximum portability, libraries that are not jQuery should +// declare themselves as anonymous modules, and avoid setting a global if an +// AMD loader is present. jQuery is a special case. For more information, see +// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon + +if ( typeof define === "function" && define.amd ) { + define( "jquery", [], function() { + return jQuery; + }); +} + + + + +var + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$; + +jQuery.noConflict = function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; } + + return jQuery; +}; + +// Expose jQuery and $ identifiers, even in +// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557) +// and CommonJS for browser emulators (#13566) +if ( typeof noGlobal === strundefined ) { + window.jQuery = window.$ = jQuery; } -})( window ); + + + +return jQuery; + +})); /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -9822,139 +9222,129 @@ if ( typeof module === "object" && module && typeof module.exports === "object" * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ -function minErr(module) { - return function () { +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { var code = arguments[0], prefix = '[' + (module ? module + ':' : '') + code + '] ', template = arguments[1], templateArgs = arguments, - stringify = function (obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return JSON.stringify(obj); - } - return obj; - }, + message, i; - message = prefix + template.replace(/\{\d+\}/g, function (match) { + message = prefix + template.replace(/\{\d+\}/g, function(match) { var index = +match.slice(1, -1), arg; if (index + 2 < templateArgs.length) { - arg = templateArgs[index + 2]; - if (typeof arg === 'function') { - return arg.toString().replace(/ ?\{[\s\S]*$/, ''); - } else if (typeof arg === 'undefined') { - return 'undefined'; - } else if (typeof arg !== 'string') { - return toJson(arg); - } - return arg; + return toDebugString(templateArgs[index + 2]); } return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.16/' + + message = message + '\nhttp://errors.angularjs.org/1.3.7/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + - encodeURIComponent(stringify(arguments[i])); + message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + + encodeURIComponent(toDebugString(arguments[i])); } - - return new Error(message); + return new ErrorConstructor(message); }; } /* We need to tell jshint what variables are being exported */ -/* global - -angular, - -msie, - -jqLite, - -jQuery, - -slice, - -push, - -toString, - -ngMinErr, - -_angular, - -angularModule, - -nodeName_, - -uid, - - -lowercase, - -uppercase, - -manualLowercase, - -manualUppercase, - -nodeName_, - -isArrayLike, - -forEach, - -sortedKeys, - -forEachSorted, - -reverseParams, - -nextUid, - -setHashKey, - -extend, - -int, - -inherit, - -noop, - -identity, - -valueFn, - -isUndefined, - -isDefined, - -isObject, - -isString, - -isNumber, - -isDate, - -isArray, - -isFunction, - -isRegExp, - -isWindow, - -isScope, - -isFile, - -isBlob, - -isBoolean, - -trim, - -isElement, - -makeMap, - -map, - -size, - -includes, - -indexOf, - -arrayRemove, - -isLeafNode, - -copy, - -shallowCopy, - -equals, - -csp, - -concat, - -sliceArgs, - -bind, - -toJsonReplacer, - -toJson, - -fromJson, - -toBoolean, - -startingTag, - -tryDecodeURIComponent, - -parseKeyValue, - -toKeyValue, - -encodeUriSegment, - -encodeUriQuery, - -angularInit, - -bootstrap, - -snake_case, - -bindJQuery, - -assertArg, - -assertArgFn, - -assertNotHasOwnProperty, - -getter, - -getBlockElements, - -hasOwnProperty, - +/* global angular: true, + msie: true, + jqLite: true, + jQuery: true, + slice: true, + splice: true, + push: true, + toString: true, + ngMinErr: true, + angularModule: true, + uid: true, + REGEX_STRING_REGEXP: true, + VALIDITY_STATE_PROPERTY: true, + + lowercase: true, + uppercase: true, + manualLowercase: true, + manualUppercase: true, + nodeName_: true, + isArrayLike: true, + forEach: true, + sortedKeys: true, + forEachSorted: true, + reverseParams: true, + nextUid: true, + setHashKey: true, + extend: true, + int: true, + inherit: true, + noop: true, + identity: true, + valueFn: true, + isUndefined: true, + isDefined: true, + isObject: true, + isString: true, + isNumber: true, + isDate: true, + isArray: true, + isFunction: true, + isRegExp: true, + isWindow: true, + isScope: true, + isFile: true, + isFormData: true, + isBlob: true, + isBoolean: true, + isPromiseLike: true, + trim: true, + escapeForRegexp: true, + isElement: true, + makeMap: true, + includes: true, + arrayRemove: true, + copy: true, + shallowCopy: true, + equals: true, + csp: true, + concat: true, + sliceArgs: true, + bind: true, + toJsonReplacer: true, + toJson: true, + fromJson: true, + startingTag: true, + tryDecodeURIComponent: true, + parseKeyValue: true, + toKeyValue: true, + encodeUriSegment: true, + encodeUriQuery: true, + angularInit: true, + bootstrap: true, + getTestability: true, + snake_case: true, + bindJQuery: true, + assertArg: true, + assertArgFn: true, + assertNotHasOwnProperty: true, + getter: true, + getBlockNodes: true, + hasOwnProperty: true, + createMap: true, + + NODE_TYPE_ELEMENT: true, + NODE_TYPE_TEXT: true, + NODE_TYPE_COMMENT: true, + NODE_TYPE_DOCUMENT: true, + NODE_TYPE_DOCUMENT_FRAGMENT: true, */ //////////////////////////////////// @@ -9974,30 +9364,36 @@ function minErr(module) { * <div doc-module-components="ng"></div> */ +var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + /** * @ngdoc function * @name angular.lowercase * @module ng - * @function + * @kind function * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ -var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; +var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase * @module ng - * @function + * @kind function * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ -var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; +var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; var manualLowercase = function(s) { @@ -10023,31 +9419,26 @@ if ('i' !== 'I'.toLowerCase()) { } -var /** holds major version number for IE or NaN for real browsers */ - msie, +var + msie, // holds major version number for IE, or NaN if UA is not IE. jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, + splice = [].splice, push = [].push, toString = Object.prototype.toString, ngMinErr = minErr('ng'), - - _angular = window.angular, /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, - nodeName_, - uid = ['0', '0', '0']; + uid = 0; /** - * IE 11 changed the format of the UserAgent string. - * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -if (isNaN(msie)) { - msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -} +msie = document.documentMode; /** @@ -10063,7 +9454,7 @@ function isArrayLike(obj) { var length = obj.length; - if (obj.nodeType === 1 && length) { + if (obj.nodeType === NODE_TYPE_ELEMENT && length) { return true; } @@ -10075,21 +9466,26 @@ function isArrayLike(obj) { * @ngdoc function * @name angular.forEach * @module ng - * @function + * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an - * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` - * is the value of an object property or an array element and `key` is the object property key or - * array element index. Specifying a `context` for the function is optional. + * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` + * is the value of an object property or an array element, `key` is the object property key or + * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. * * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. * + * 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 + * return the value provided. + * ```js var values = {name: 'misko', gender: 'male'}; var log = []; - angular.forEach(values, function(value, key){ + angular.forEach(values, function(value, key) { this.push(key + ': ' + value); }, log); expect(log).toEqual(['name: misko', 'gender: male']); @@ -10100,26 +9496,31 @@ function isArrayLike(obj) { * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ + function forEach(obj, iterator, context) { - var key; + var key, length; if (obj) { - if (isFunction(obj)){ + if (isFunction(obj)) { for (key in obj) { // Need to check if hasOwnProperty exists, // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); + } + } + } else if (isArray(obj) || isArrayLike(obj)) { + var isPrimitive = typeof obj !== 'object'; + for (key = 0, length = obj.length; key < length; key++) { + if (isPrimitive || key in obj) { + iterator.call(context, obj[key], key, obj); } } } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { - for (key = 0; key < obj.length; key++) - iterator.call(context, obj[key], key); + obj.forEach(iterator, context, obj); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); } } } @@ -10128,18 +9529,12 @@ function forEach(obj, iterator, context) { } function sortedKeys(obj) { - var keys = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key); - } - } - return keys.sort(); + return Object.keys(obj).sort(); } function forEachSorted(obj, iterator, context) { var keys = sortedKeys(obj); - for ( var i = 0; i < keys.length; i++) { + for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } return keys; @@ -10156,33 +9551,17 @@ function reverseParams(iteratorFn) { } /** - * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric - * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the nextId - * will grow much slower, it is a string, and it will never overflow. + * A consistent way of creating unique IDs in angular. + * + * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before + * we hit number precision issues in JavaScript. * - * @returns {string} an unique alpha-numeric string + * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M + * + * @returns {number} an unique alpha-numeric string */ function nextUid() { - var index = uid.length; - var digit; - - while(index) { - index--; - digit = uid[index].charCodeAt(0); - if (digit == 57 /*'9'*/) { - uid[index] = 'A'; - return uid.join(''); - } - if (digit == 90 /*'Z'*/) { - uid[index] = '0'; - } else { - uid[index] = String.fromCharCode(digit + 1); - return uid.join(''); - } - } - uid.unshift('0'); - return uid.join(''); + return ++uid; } @@ -10204,11 +9583,13 @@ function setHashKey(obj, h) { * @ngdoc function * @name angular.extend * @module ng - * @function + * @kind function * * @description - * Extends the destination object `dst` by copying all of the properties from the `src` object(s) - * to `dst`. You can specify multiple `src` objects. + * 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). * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). @@ -10216,15 +9597,19 @@ function setHashKey(obj, h) { */ function extend(dst) { var h = dst.$$hashKey; - forEach(arguments, function(obj){ - if (obj !== dst) { - forEach(obj, function(value, key){ - dst[key] = value; - }); + + 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); + setHashKey(dst, h); return dst; } @@ -10234,14 +9619,14 @@ function int(str) { function inherit(parent, extra) { - return extend(new (extend(function() {}, {prototype:parent}))(), extra); + return extend(Object.create(parent), extra); } /** * @ngdoc function * @name angular.noop * @module ng - * @function + * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the @@ -10261,7 +9646,7 @@ noop.$inject = []; * @ngdoc function * @name angular.identity * @module ng - * @function + * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the @@ -10283,7 +9668,7 @@ function valueFn(value) {return function() {return value;};} * @ngdoc function * @name angular.isUndefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is undefined. @@ -10291,14 +9676,14 @@ function valueFn(value) {return function() {return value;};} * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ -function isUndefined(value){return typeof value === 'undefined';} +function isUndefined(value) {return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is defined. @@ -10306,14 +9691,14 @@ function isUndefined(value){return typeof value === 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ -function isDefined(value){return typeof value !== 'undefined';} +function isDefined(value) {return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject * @module ng - * @function + * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not @@ -10322,14 +9707,17 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value === 'object';} +function isObject(value) { + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; +} /** * @ngdoc function * @name angular.isString * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `String`. @@ -10337,14 +9725,14 @@ function isObject(value){return value != null && typeof value === 'object';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ -function isString(value){return typeof value === 'string';} +function isString(value) {return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Number`. @@ -10352,14 +9740,14 @@ function isString(value){return typeof value === 'string';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ -function isNumber(value){return typeof value === 'number';} +function isNumber(value) {return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate * @module ng - * @function + * @kind function * * @description * Determines if a value is a date. @@ -10367,7 +9755,7 @@ function isNumber(value){return typeof value === 'number';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ -function isDate(value){ +function isDate(value) { return toString.call(value) === '[object Date]'; } @@ -10376,7 +9764,7 @@ function isDate(value){ * @ngdoc function * @name angular.isArray * @module ng - * @function + * @kind function * * @description * Determines if a reference is an `Array`. @@ -10384,16 +9772,13 @@ function isDate(value){ * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -function isArray(value) { - return toString.call(value) === '[object Array]'; -} - +var isArray = Array.isArray; /** * @ngdoc function * @name angular.isFunction * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Function`. @@ -10401,7 +9786,7 @@ function isArray(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ -function isFunction(value){return typeof value === 'function';} +function isFunction(value) {return typeof value === 'function';} /** @@ -10424,7 +9809,7 @@ function isRegExp(value) { * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { - return obj && obj.document && obj.location && obj.alert && obj.setInterval; + return obj && obj.window === obj; } @@ -10438,6 +9823,11 @@ function isFile(obj) { } +function isFormData(obj) { + return toString.call(obj) === '[object FormData]'; +} + + function isBlob(obj) { return toString.call(obj) === '[object Blob]'; } @@ -10448,26 +9838,29 @@ function isBoolean(value) { } -var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; -})(); +function isPromiseLike(obj) { + return obj && isFunction(obj.then); +} + + +var trim = function(value) { + return isString(value) ? value.trim() : value; +}; + +// Copied from: +// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 +// Prereq: s is a string. +var escapeForRegexp = function(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). + replace(/\x08/g, '\\x08'); +}; /** * @ngdoc function * @name angular.isElement * @module ng - * @function + * @kind function * * @description * Determines if a reference is a DOM element (or wrapped jQuery element). @@ -10485,100 +9878,34 @@ function isElement(node) { * @param str 'key1,key2,...' * @returns {object} in the form of {key1:true, key2:true, ...} */ -function makeMap(str){ +function makeMap(str) { var obj = {}, items = str.split(","), i; - for ( i = 0; i < items.length; i++ ) + for (i = 0; i < items.length; i++) obj[ items[i] ] = true; return obj; } -if (msie < 9) { - nodeName_ = function(element) { - element = element.nodeName ? element : element[0]; - return (element.scopeName && element.scopeName != 'HTML') - ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; - }; -} else { - nodeName_ = function(element) { - return element.nodeName ? element.nodeName : element[0].nodeName; - }; -} - - -function map(obj, iterator, context) { - var results = []; - forEach(obj, function(value, index, list) { - results.push(iterator.call(context, value, index, list)); - }); - return results; +function nodeName_(element) { + return lowercase(element.nodeName || (element[0] && element[0].nodeName)); } - -/** - * @description - * Determines the number of elements in an array, the number of properties an object has, or - * the length of a string. - * - * Note: This function is used to augment the Object type in Angular expressions. See - * {@link angular.Object} for more information about Angular arrays. - * - * @param {Object|Array|string} obj Object, array, or string to inspect. - * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object - * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. - */ -function size(obj, ownPropsOnly) { - var count = 0, key; - - if (isArray(obj) || isString(obj)) { - return obj.length; - } else if (isObject(obj)){ - for (key in obj) - if (!ownPropsOnly || obj.hasOwnProperty(key)) - count++; - } - - return count; -} - - function includes(array, obj) { - return indexOf(array, obj) != -1; -} - -function indexOf(array, obj) { - if (array.indexOf) return array.indexOf(obj); - - for (var i = 0; i < array.length; i++) { - if (obj === array[i]) return i; - } - return -1; + return Array.prototype.indexOf.call(array, obj) != -1; } function arrayRemove(array, value) { - var index = indexOf(array, value); - if (index >=0) + var index = array.indexOf(value); + if (index >= 0) array.splice(index, 1); return value; } -function isLeafNode (node) { - if (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": - return true; - } - } - return false; -} - /** * @ngdoc function * @name angular.copy * @module ng - * @function + * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. @@ -10596,9 +9923,9 @@ function isLeafNode (node) { * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - <example> + <example module="copyExample"> <file name="index.html"> - <div ng-controller="Controller"> + <div ng-controller="ExampleController"> <form novalidate class="simple-form"> Name: <input type="text" ng-model="user.name" /><br /> E-mail: <input type="email" ng-model="user.email" /><br /> @@ -10612,26 +9939,27 @@ function isLeafNode (node) { </div> <script> - function Controller($scope) { - $scope.master= {}; + angular.module('copyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.master= {}; - $scope.update = function(user) { - // Example with 1 argument - $scope.master= angular.copy(user); - }; + $scope.update = function(user) { + // Example with 1 argument + $scope.master= angular.copy(user); + }; - $scope.reset = function() { - // Example with 2 arguments - angular.copy($scope.master, $scope.user); - }; + $scope.reset = function() { + // Example with 2 arguments + angular.copy($scope.master, $scope.user); + }; - $scope.reset(); - } + $scope.reset(); + }]); </script> </file> </example> */ -function copy(source, destination){ +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."); @@ -10641,52 +9969,92 @@ function copy(source, destination){ destination = source; if (source) { if (isArray(source)) { - destination = copy(source, []); + destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { - destination = new RegExp(source.source); + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; } else if (isObject(source)) { - destination = copy(source, {}); + var emptyObject = Object.create(Object.getPrototypeOf(source)); + destination = copy(source, emptyObject, stackSource, stackDest); } } } else { if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = stackSource.indexOf(source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; if (isArray(source)) { destination.length = 0; - for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + 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); } } else { var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); - for ( var key in source) { - destination[key] = copy(source[key]); + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + delete destination[key]; + }); + } + for (var 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; + } } setHashKey(destination,h); } + } return destination; } /** - * Create a shallow copy of an object + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. */ function shallowCopy(src, dst) { - dst = dst || {}; + if (isArray(src)) { + dst = dst || []; - for(var key in src) { - // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src - // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } } } - return dst; + return dst || src; } @@ -10694,7 +10062,7 @@ function shallowCopy(src, dst) { * @ngdoc function * @name angular.equals * @module ng - * @function + * @kind function * * @description * Determines if two objects or two values are equivalent. Supports value types, regular @@ -10706,7 +10074,7 @@ function shallowCopy(src, dst) { * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, + * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * @@ -10729,24 +10097,25 @@ function equals(o1, o2) { if (isArray(o1)) { if (!isArray(o2)) return false; if ((length = o1.length) == o2.length) { - for(key=0; key<length; key++) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; } return true; } } else if (isDate(o1)) { - return isDate(o2) && o1.getTime() == o2.getTime(); + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); } else if (isRegExp(o1) && isRegExp(o2)) { return o1.toString() == o2.toString(); } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2)) return false; keySet = {}; - for(key in o1) { + 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) { + for (key in o2) { if (!keySet.hasOwnProperty(key) && key.charAt(0) !== '$' && o2[key] !== undefined && @@ -10759,12 +10128,25 @@ function equals(o1, o2) { return false; } +var csp = function() { + if (isDefined(csp.isActive_)) return csp.isActive_; + + var active = !!(document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); + + if (!active) { + try { + /* jshint -W031, -W054 */ + new Function(''); + /* jshint +W031, +W054 */ + } catch (e) { + active = true; + } + } + + return (csp.isActive_ = active); +}; -function csp() { - return (document.securityPolicy && document.securityPolicy.isActive) || - (document.querySelector && - !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); -} function concat(array1, array2, index) { @@ -10781,7 +10163,7 @@ function sliceArgs(args, startIndex) { * @ngdoc function * @name angular.bind * @module ng - * @function + * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for @@ -10801,7 +10183,7 @@ function bind(self, fn) { return curryArgs.length ? function() { return arguments.length - ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) + ? fn.apply(self, concat(curryArgs, arguments, 0)) : fn.apply(self, curryArgs); } : function() { @@ -10819,7 +10201,7 @@ function bind(self, fn) { function toJsonReplacer(key, value) { var val = value; - if (typeof key === 'string' && key.charAt(0) === '$') { + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -10837,19 +10219,23 @@ function toJsonReplacer(key, value) { * @ngdoc function * @name angular.toJson * @module ng - * @function + * @kind function * * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @param {boolean|number=} pretty If set to true, the JSON output will contain newlines and whitespace. + * If set to an integer, the JSON output will contain that many spaces per indentation (the default is 2). * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { if (typeof obj === 'undefined') return undefined; - return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); } @@ -10857,7 +10243,7 @@ function toJson(obj, pretty) { * @ngdoc function * @name angular.fromJson * @module ng - * @function + * @kind function * * @description * Deserializes a JSON string. @@ -10872,18 +10258,6 @@ function fromJson(json) { } -function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; -} - /** * @returns {string} Returns the string representation of the element. */ @@ -10893,16 +10267,14 @@ function startingTag(element) { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. element.empty(); - } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; + } catch (e) {} var elemHtml = jqLite('<div>').append(element).html(); try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : + return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch(e) { + } catch (e) { return lowercase(elemHtml); } @@ -10922,7 +10294,7 @@ function startingTag(element) { function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); - } catch(e) { + } catch (e) { // Ignore any invalid uri component } } @@ -10934,15 +10306,15 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { - key_value = keyValue.split('='); + forEach((keyValue || "").split('&'), function(keyValue) { + if (keyValue) { + key_value = keyValue.replace(/\+/g,'%20').split('='); key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { + if (isDefined(key)) { var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { + if (!hasOwnProperty.call(obj, key)) { obj[key] = val; - } else if(isArray(obj[key])) { + } else if (isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; @@ -11006,9 +10378,23 @@ function encodeUriQuery(val, pctEncodeSpaces) { replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). + replace(/%3B/gi, ';'). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } +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))) { + return attr; + } + } + return null; +} /** * @ngdoc directive @@ -11018,6 +10404,11 @@ function encodeUriQuery(val, pctEncodeSpaces) { * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. + * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be + * created in "strict-di" mode. This means that the application will fail to invoke functions which + * do not use explicit function annotation (and are thus unsuitable for minification), as described + * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in + * tracking down the root of these bugs. * * @description * @@ -11055,48 +10446,109 @@ function encodeUriQuery(val, pctEncodeSpaces) { </file> </example> * + * Using `ngStrictDi`, you would see something like this: + * + <example ng-app-included="true"> + <file name="index.html"> + <div ng-app="ngAppStrictDemo" ng-strict-di> + <div ng-controller="GoodController1"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) + </p> + </div> + + <div ng-controller="GoodController2"> + Name: <input ng-model="name"><br /> + Hello, {{name}}! + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) + </p> + </div> + + <div ng-controller="BadController"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. + </p> + </div> + </div> + </file> + <file name="script.js"> + angular.module('ngAppStrictDemo', []) + // BadController will fail to instantiate, due to relying on automatic function annotation, + // rather than an explicit annotation + .controller('BadController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }) + // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, + // due to using explicit annotations using the array style and $inject property, respectively. + .controller('GoodController1', ['$scope', function($scope) { + $scope.a = 1; + $scope.b = 2; + }]) + .controller('GoodController2', GoodController2); + function GoodController2($scope) { + $scope.name = "World"; + } + GoodController2.$inject = ['$scope']; + </file> + <file name="style.css"> + div[ng-controller] { + margin-bottom: 1em; + -webkit-border-radius: 4px; + border-radius: 4px; + border: 1px solid; + padding: .5em; + } + div[ng-controller^=Good] { + border-color: #d6e9c6; + background-color: #dff0d8; + color: #3c763d; + } + div[ng-controller^=Bad] { + border-color: #ebccd1; + background-color: #f2dede; + color: #a94442; + margin-bottom: 0; + } + </file> + </example> */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, + var appElement, module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { - bootstrap(appElement, module ? [module] : []); + config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + bootstrap(appElement, module ? [module] : [], config); } } @@ -11109,68 +10561,81 @@ function angularInit(element, bootstrap) { * * See: {@link guide/bootstrap Bootstrap} * - * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. + * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for - * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * - * <example name="multi-bootstrap" module="multi-bootstrap"> - * <file name="index.html"> - * <script src="../../../angular.js"></script> - * <div ng-controller="BrokenTable"> - * <table> - * <tr> - * <th ng-repeat="heading in headings">{{heading}}</th> - * </tr> - * <tr ng-repeat="filling in fillings"> - * <td ng-repeat="fill in filling">{{fill}}</td> - * </tr> - * </table> + * ```html + * <!doctype html> + * <html> + * <body> + * <div ng-controller="WelcomeController"> + * {{greeting}} * </div> - * </file> - * <file name="controller.js"> - * var app = angular.module('multi-bootstrap', []) * - * .controller('BrokenTable', function($scope) { - * $scope.headings = ['One', 'Two', 'Three']; - * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; - * }); - * </file> - * <file name="protractor.js" type="protractor"> - * it('should only insert one table cell for each item in $scope.fillings', function() { - * expect(element.all(by.css('td')).count()) - * .toBe(9); - * }); - * </file> - * </example> + * <script src="angular.js"></script> + * <script> + * var app = angular.module('demo', []) + * .controller('WelcomeController', function($scope) { + * $scope.greeting = 'Welcome!'; + * }); + * angular.bootstrap(document, ['demo']); + * </script> + * </body> + * </html> + * ``` * * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array<String|Function|Array>=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) * function that will be invoked by the injector as a run block. * See: {@link angular.module modules} + * @param {Object=} config an object for defining configuration options for the application. The + * following keys are supported: + * + * * `strictDi` - disable automatic function annotation for the application. This is meant to + * assist in finding bugs which break minified code. Defaults to `false`. + * * @returns {auto.$injector} Returns the newly created injector for this app. */ -function bootstrap(element, modules) { +function bootstrap(element, modules, config) { + if (!isObject(config)) config = {}; + var defaultConfig = { + strictDi: false + }; + config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + //Encode angle brackets to prevent input from being sanitized to empty string #8683 + throw ngMinErr( + 'btstrpd', + "App Already Bootstrapped with this Element '{0}'", + tag.replace(/</,'<').replace(/>/,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { + var injector = createInjector(modules, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -11180,8 +10645,14 @@ function bootstrap(element, modules) { return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -11195,19 +10666,62 @@ function bootstrap(element, modules) { }; } +/** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ +function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + window.location.reload(); +} + +/** + * @name angular.getTestability + * @module ng + * @description + * Get the testability service for the instance of Angular on the given + * element. + * @param {DOMElement} element DOM element which is the root of angular application. + */ +function getTestability(rootElement) { + var injector = angular.element(rootElement).injector(); + if (!injector) { + throw ngMinErr('test', + 'no injector found for element argument to getTestability'); + } + return injector.get('$$testability'); +} + var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator){ +function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } +var bindJQueryFired = false; +var skipDestroyOnNextJQueryCleanData; function bindJQuery() { + var originalCleanData; + + if (bindJQueryFired) { + return; + } + // bind to jQuery if present; jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + // 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 + // versions. It will not work for sure with jQuery <1.7, though. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, @@ -11216,15 +10730,33 @@ function bindJQuery() { injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); + + // All nodes removed from the DOM via various jQuery APIs like .remove() + // are passed through jQuery.cleanData. Monkey-patch this method to fire + // the $destroy event on all removed nodes. + originalCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + var events; + if (!skipDestroyOnNextJQueryCleanData) { + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, "events"); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } + } + } else { + skipDestroyOnNextJQueryCleanData = false; + } + originalCleanData(elems); + }; } else { jqLite = JQLite; } + angular.element = jqLite; + + // Prevent double-proxying. + bindJQueryFired = true; } /** @@ -11243,7 +10775,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } @@ -11288,27 +10820,46 @@ 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 {DOMElement} object containing the elements + * @returns {jqLite} jqLite collection containing the nodes */ -function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); - } - - var element = startNode; - var elements = [element]; +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. + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes = [node]; do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); + node = node.nextSibling; + if (!node) break; + blockNodes.push(node); + } while (node !== endNode); - return jqLite(elements); + return jqLite(blockNodes); } + +/** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ +function createMap() { + return Object.create(null); +} + +var NODE_TYPE_ELEMENT = 1; +var NODE_TYPE_TEXT = 3; +var NODE_TYPE_COMMENT = 8; +var NODE_TYPE_DOCUMENT = 9; +var NODE_TYPE_DOCUMENT_FRAGMENT = 11; + /** * @ngdoc type * @name angular.Module @@ -11353,7 +10904,7 @@ function setupModuleLoader(window) { * * # Module * - * A module is a collection of services, directives, filters, and configuration information. + * A module is a collection of services, directives, controllers, filters, and configuration information. * `angular.module` is used to configure the {@link auto.$injector $injector}. * * ```js @@ -11381,9 +10932,9 @@ function setupModuleLoader(window) { * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. -<<<<<* @param {!Array.<string>=} requires If specified then new module is being created. If ->>>>>* unspecified then the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as + * @param {!Array.<string>=} requires If specified then new module is being created. If + * 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. */ @@ -11409,21 +10960,25 @@ function setupModuleLoader(window) { var invokeQueue = []; /** @type {!Array.<Function>} */ + var configBlocks = []; + + /** @type {!Array.<Function>} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, /** * @ngdoc property * @name angular.Module#requires * @module ng - * @returns {Array.<string>} List of module names which must be loaded before this module. + * * @description * Holds the list of modules which the injector will load before the current module is * loaded. @@ -11434,8 +10989,9 @@ function setupModuleLoader(window) { * @ngdoc property * @name angular.Module#name * @module ng - * @returns {string} Name of the module. + * * @description + * Name of the module. */ name: name, @@ -11526,7 +11082,7 @@ function setupModuleLoader(window) { * }) * ``` * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ animation: invokeLater('$animateProvider', 'register'), @@ -11575,6 +11131,8 @@ function setupModuleLoader(window) { * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. */ config: config, @@ -11598,7 +11156,7 @@ function setupModuleLoader(window) { config(configFn); } - return moduleInstance; + return moduleInstance; /** * @param {string} provider @@ -11606,9 +11164,10 @@ function setupModuleLoader(window) { * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } @@ -11618,81 +11177,119 @@ function setupModuleLoader(window) { } -/* global - angularModule: true, - version: true, - - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $$RAFProvider, - $$AsyncCallbackProvider, - $WindowProvider +/* 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 '<<already seen>>'; + + seen.push(val); + } + return val; + }); +} + +function toDebugString(obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (typeof obj === 'undefined') { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj); + } + return obj; +} + +/* global angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider, + $$jqLiteProvider */ @@ -11711,15 +11308,15 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.16', // all of these placeholder strings will be replaced by grunt's + full: '1.3.7', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 2, - dot: 16, - codeName: 'badger-enumeration' + minor: 3, + dot: 7, + codeName: 'leaky-obstruction' }; -function publishExternalAPI(angular){ +function publishExternalAPI(angular) { extend(angular, { 'bootstrap': bootstrap, 'copy': copy, @@ -11728,11 +11325,11 @@ function publishExternalAPI(angular){ 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -11746,8 +11343,10 @@ function publishExternalAPI(angular){ 'lowercase': lowercase, 'uppercase': uppercase, 'callbacks': {counter: 0}, + 'getTestability': getTestability, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + 'reloadWithDebugInfo': reloadWithDebugInfo }); angularModule = setupModuleLoader(window); @@ -11799,9 +11398,16 @@ function publishExternalAPI(angular){ ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, required: requiredDirective, ngRequired: requiredDirective, - ngValue: ngValueDirective + minlength: minlengthDirective, + ngMinlength: minlengthDirective, + maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, + ngValue: ngValueDirective, + ngModelOptions: ngModelOptionsDirective }). directive({ ngInclude: ngIncludeFillContentDirective @@ -11826,25 +11432,28 @@ function publishExternalAPI(angular){ $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, + $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, - $$asyncCallback : $$AsyncCallbackProvider + $$asyncCallback: $$AsyncCallbackProvider, + $$jqLite: $$jqLiteProvider }); } ]); } -/* global - - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true, */ ////////////////////////////////// @@ -11855,7 +11464,7 @@ function publishExternalAPI(angular){ * @ngdoc function * @name angular.element * @module ng - * @function + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. @@ -11879,13 +11488,14 @@ function publishExternalAPI(angular){ * - [`addClass()`](http://api.jquery.com/addClass/) * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) + * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData * - [`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/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()` * - [`data()`](http://api.jquery.com/data/) + * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -11926,10 +11536,12 @@ function publishExternalAPI(angular){ * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current - * element or its parent. + * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to + * be enabled. * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. + * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * @@ -11937,20 +11549,21 @@ function publishExternalAPI(angular){ * @returns {Object} jQuery object. */ +JQLite.expando = 'ng339'; + var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + addEventListenerFn = function(element, type, fn) { + element.addEventListener(type, fn, false); + }, + removeEventListenerFn = function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; /* * !!! This is an undocumented "private" function !!! */ -var jqData = JQLite._data = function(node) { +JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; @@ -11960,6 +11573,7 @@ function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; var jqLiteMinErr = minErr('jqLite'); /** @@ -11975,49 +11589,6 @@ function camelCase(name) { replace(MOZ_HACK_REGEXP, 'Moz$1'); } -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// - -function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; - - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; - - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); - } -} - var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; var TAG_NAME_REGEXP = /<([\w:]+)/; @@ -12037,26 +11608,32 @@ wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; + function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } +function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + var nodeType = node.nodeType; + return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; +} + function jqLiteBuildFragment(html, context) { - var elem, tmp, tag, wrap, + var tmp, tag, wrap, fragment = context.createDocumentFragment(), - nodes = [], i, j, jj; + nodes = [], i; if (jqLiteIsTextNode(html)) { // Convert non-html into a text node nodes.push(context.createTextNode(html)); } else { - tmp = fragment.appendChild(context.createElement('div')); // Convert html into DOM nodes + tmp = tmp || fragment.appendChild(context.createElement("div")); tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = '<div> </div>' + - wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; - tmp.removeChild(tmp.firstChild); + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -12064,7 +11641,7 @@ function jqLiteBuildFragment(html, context) { tmp = tmp.lastChild; } - for (j=0, jj=tmp.childNodes.length; j<jj; ++j) nodes.push(tmp.childNodes[j]); + nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; tmp.textContent = ""; @@ -12073,7 +11650,11 @@ function jqLiteBuildFragment(html, context) { // Remove wrapper from fragment fragment.textContent = ""; fragment.innerHTML = ""; // Clear inner HTML - return nodes; + forEach(nodes, function(node) { + fragment.appendChild(node); + }); + + return fragment; } function jqLiteParseHTML(html, context) { @@ -12084,7 +11665,11 @@ function jqLiteParseHTML(html, context) { return [context.createElement(parsed[1])]; } - return jqLiteBuildFragment(html, context); + if ((parsed = jqLiteBuildFragment(html, context))) { + return parsed.childNodes; + } + + return []; } ///////////////////////////////////////////// @@ -12092,20 +11677,22 @@ function JQLite(element) { if (element instanceof JQLite) { return element; } + + var argIsString; + if (isString(element)) { element = trim(element); + argIsString = true; } if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { + if (argIsString && element.charAt(0) != '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } - if (isString(element)) { + if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); - var fragment = jqLite(document.createDocumentFragment()); - fragment.append(this); } else { jqLiteAddNodes(this, element); } @@ -12115,94 +11702,106 @@ function jqLiteClone(element) { return element.cloneNode(true); } -function jqLiteDealoc(element){ - jqLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - jqLiteDealoc(children[i]); +function jqLiteDealoc(element, onlyDescendants) { + if (!onlyDescendants) jqLiteRemoveData(element); + + if (element.querySelectorAll) { + var descendants = element.querySelectorAll('*'); + for (var i = 0, l = descendants.length; i < l; i++) { + jqLiteRemoveData(descendants[i]); + } } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; if (!handle) return; //no listeners registered - if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); + if (!type) { + for (type in events) { + if (type !== '$destroy') { + removeEventListenerFn(element, type, handle); + } delete events[type]; - }); + } } else { forEach(type.split(' '), function(type) { - if (isUndefined(fn)) { - removeEventListenerFn(element, type, events[type]); - delete events[type]; - } else { - arrayRemove(events[type] || [], fn); + if (isDefined(fn)) { + var listenerFns = events[type]; + arrayRemove(listenerFns || [], fn); + if (listenerFns && listenerFns.length > 0) { + return; + } } + + removeEventListenerFn(element, type, handle); + delete events[type]; }); } } function jqLiteRemoveData(element, name) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } } -function jqLiteExpandoStore(element, key, value) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId || -1]; - if (isDefined(value)) { - if (!expandoStore) { - element[jqName] = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; +function jqLiteExpandoStore(element, createIfNecessary) { + var expandoId = element.ng339, + expandoStore = expandoId && jqCache[expandoId]; + + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; + if (isSimpleSetter) { // data('key', value) + data[key] = value; + } else { + if (massGetter) { // data() + return data; } else { - extend(data, key); + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet + return data && data[key]; + } else { // mass-setter: data({key1: val1, key2: val2}) + extend(data, key); + } } - } else { - return data; } } } @@ -12210,7 +11809,7 @@ function jqLiteData(element, key, value) { function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf( " " + selector + " " ) > -1); + indexOf(" " + selector + " ") > -1); } function jqLiteRemoveClass(element, cssClasses) { @@ -12241,53 +11840,84 @@ function jqLiteAddClass(element, cssClasses) { } } + function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); + + // if a Node (the most common case) + if (elements.nodeType) { + root[root.length++] = elements; + } else { + var length = elements.length; + + // if an Array or NodeList and not a Window + if (typeof length === 'number' && elements.window !== elements) { + if (length) { + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; + } + } + } else { + root[root.length++] = elements; + } } } } + function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); + return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if (element.nodeType == NODE_TYPE_DOCUMENT) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - var node = element[0]; + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. - element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } +function jqLiteRemove(element, keepData) { + if (!keepData) jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); +} + + +function jqLiteDocumentLoaded(action, win) { + win = win || window; + if (win.document.readyState === 'complete') { + // Force the action to be run async for consistent behaviour + // from the action's point of view + // i.e. it will definitely not be in a $apply + win.setTimeout(action); + } else { + // No need to unbind this handler as load is only ever called once + jqLite(win).on('load', action); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// @@ -12301,8 +11931,8 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - // check if document already is loaded - if (document.readyState === 'complete'){ + // check if document is already loaded + if (document.readyState === 'complete') { setTimeout(trigger); } else { this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 @@ -12314,7 +11944,7 @@ var JQLitePrototype = JQLite.prototype = { }, toString: function() { var value = []; - forEach(this, function(e){ value.push('' + e);}); + forEach(this, function(e) { value.push('' + e);}); return '[' + value.join(', ') + ']'; }, @@ -12339,29 +11969,48 @@ forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); +var ALIASED_ATTR = { + 'ngMinlength': 'minlength', + 'ngMaxlength': 'maxlength', + 'ngMin': 'min', + 'ngMax': 'max', + 'ngPattern': 'pattern' +}; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; +} + +function getAliasedAttrName(element, name) { + var nodeName = element.nodeName; + return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, + removeData: jqLiteRemoveData +}, function(fn, name) { + JQLite[name] = fn; +}); + +forEach({ + data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, @@ -12370,7 +12019,7 @@ forEach({ return jqLiteInheritedData(element, '$injector'); }, - removeAttr: function(element,name) { + removeAttr: function(element, name) { element.removeAttribute(name); }, @@ -12382,26 +12031,11 @@ forEach({ if (isDefined(value)) { element.style[name] = value; } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; + return element.style[name]; } }, - attr: function(element, name, value){ + attr: function(element, name, value) { var lowercasedName = lowercase(name); if (BOOLEAN_ATTR[lowercasedName]) { if (isDefined(value)) { @@ -12414,7 +12048,7 @@ forEach({ } } else { return (element[name] || - (element.attributes.getNamedItem(name)|| noop).specified) + (element.attributes.getNamedItem(name) || noop).specified) ? lowercasedName : undefined; } @@ -12438,31 +12072,23 @@ forEach({ }, text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } getText.$dv = ''; return getText; function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; if (isUndefined(value)) { - return textProp ? element[textProp] : ''; + var nodeType = element.nodeType; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } - element[textProp] = value; + element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; - forEach(element.options, function (option) { + forEach(element.options, function(option) { if (option.selected) { result.push(option.value || option.text); } @@ -12478,19 +12104,18 @@ forEach({ if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty -}, function(fn, name){ +}, function(fn, name) { /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; + var nodeCount = this.length; // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. @@ -12500,7 +12125,7 @@ forEach({ if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); @@ -12514,9 +12139,10 @@ forEach({ return this; } else { // we are a read, so read the first child. + // 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(this.length, 1) : this.length; + var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -12525,7 +12151,7 @@ forEach({ } } else { // we are a write, so apply to all children - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining @@ -12535,57 +12161,50 @@ forEach({ }); function createEventHandler(element, events) { - var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } + var eventHandler = function(event, type) { + // jQuery specific api + event.isDefaultPrevented = function() { + return event.defaultPrevented; + }; - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; - if (!event.target) { - event.target = event.srcElement || document; - } + if (!eventFnsLength) return; + + if (isUndefined(event.immediatePropagationStopped)) { + var originalStopImmediatePropagation = event.stopImmediatePropagation; + event.stopImmediatePropagation = function() { + event.immediatePropagationStopped = true; - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); + if (event.stopPropagation) { + event.stopPropagation(); + } + + if (originalStopImmediatePropagation) { + originalStopImmediatePropagation.call(event); + } }; - event.defaultPrevented = false; } - event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + event.isImmediatePropagationStopped = function() { + return event.immediatePropagationStopped === true; }; // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); - - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; + for (var i = 0; i < eventFnsLength; i++) { + if (!event.isImmediatePropagationStopped()) { + eventFns[i].call(element, event); + } } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } @@ -12598,68 +12217,56 @@ function createEventHandler(element, events) { forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; - forEach(type.split(' '), function(type){ + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } + + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; + var i = types.length; + + while (i--) { + type = types[i]; var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; + 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 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - onFn(element, eventmap[type], function(event) { + 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 && !contains(target, related)) ){ + if (!related || (related !== target && !target.contains(related))) { handle(event, type); } }); } else { - addEventListenerFn(element, type, handle); - events[type] = []; + if (type !== '$destroy') { + addEventListenerFn(element, type, handle); + } } eventFns = events[type]; } eventFns.push(fn); - }); + } }, off: jqLiteOff, @@ -12680,7 +12287,7 @@ forEach({ replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ + forEach(new JQLite(replaceNode), function(node) { if (index) { parent.insertBefore(node, index.nextSibling); } else { @@ -12692,8 +12299,8 @@ forEach({ children: function(element) { var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeType === 1) + forEach(element.childNodes, function(element) { + if (element.nodeType === NODE_TYPE_ELEMENT) children.push(element); }); return children; @@ -12704,24 +12311,28 @@ forEach({ }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { - if (element.nodeType === 1) { + if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; - forEach(new JQLite(node), function(child){ + forEach(new JQLite(node), function(child) { element.insertBefore(child, index); }); } }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; + wrapNode = jqLite(wrapNode).eq(0).clone()[0]; var parent = element.parentNode; if (parent) { parent.replaceChild(wrapNode, element); @@ -12729,18 +12340,21 @@ forEach({ wrapNode.appendChild(element); }, - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; parent.insertBefore(node, index.nextSibling); index = node; - }); + } }, addClass: jqLiteAddClass, @@ -12748,7 +12362,7 @@ forEach({ toggleClass: function(element, selector, condition) { if (selector) { - forEach(selector.split(' '), function(className){ + forEach(selector.split(' '), function(className) { var classCondition = condition; if (isUndefined(classCondition)) { classCondition = !jqLiteHasClass(element, className); @@ -12760,20 +12374,11 @@ forEach({ parent: function(element) { var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { @@ -12786,27 +12391,50 @@ forEach({ clone: jqLiteClone, - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { + + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; + + if (eventFns) { + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, + isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; - eventData = eventData || []; + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); + forEach(eventFnsCopy, function(fn) { + if (!dummyEvent.isImmediatePropagationStopped()) { + fn.apply(element, handlerArgs); + } + }); + } } -}, function(fn, name){ +}, function(fn, name) { /** * chaining functions */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + + for (var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { @@ -12825,6 +12453,27 @@ forEach({ JQLite.prototype.unbind = JQLite.prototype.off; }); + +// Provider for private $$jqLite service +function $$jqLiteProvider() { + this.$get = function $$jqLite() { + return extend(JQLite, { + hasClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteHasClass(node, classes); + }, + addClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteAddClass(node, classes); + }, + removeClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteRemoveClass(node, classes); + } + }); + }; +} + /** * Computes a hash of an 'obj'. * Hash of a: @@ -12837,28 +12486,36 @@ forEach({ * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ -function hashKey(obj) { - var objType = typeof obj, - key; +function hashKey(obj, nextUidFn) { + var key = obj && obj.$$hashKey; - if (objType == 'object' && obj !== null) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this + if (key) { + if (typeof key === 'function') { key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); } + return key; + } + + var objType = typeof obj; + if (objType == 'function' || (objType == 'object' && obj !== null)) { + key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { - key = obj; + key = objType + ':' + obj; } - return objType + ':' + key; + return key; } /** * HashMap which can use objects as keys */ -function HashMap(array){ +function HashMap(array, isolatedUid) { + if (isolatedUid) { + var uid = 0; + this.nextUid = function() { + return ++uid; + }; + } forEach(array, this.put, this); } HashMap.prototype = { @@ -12868,7 +12525,7 @@ HashMap.prototype = { * @param value value to store can be any type */ put: function(key, value) { - this[hashKey(key)] = value; + this[hashKey(key, this.nextUid)] = value; }, /** @@ -12876,7 +12533,7 @@ HashMap.prototype = { * @returns {Object} the value for the key */ get: function(key) { - return this[hashKey(key)]; + return this[hashKey(key, this.nextUid)]; }, /** @@ -12884,7 +12541,7 @@ HashMap.prototype = { * @param key */ remove: function(key) { - var value = this[key = hashKey(key)]; + var value = this[key = hashKey(key, this.nextUid)]; delete this[key]; return value; } @@ -12894,16 +12551,17 @@ HashMap.prototype = { * @ngdoc function * @module ng * @name angular.injector - * @function + * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * - * @param {Array.<string|Function>} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link auto.$injector $injector}. + * {@link angular.module}. The `ng` module must be explicitly added. + * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which + * disallows argument name annotation inference. + * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage @@ -12913,7 +12571,7 @@ HashMap.prototype = { * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document){ + * $injector.invoke(function($rootScope, $compile, $document) { * $compile($document)($rootScope); * $rootScope.$digest(); * }); @@ -12921,7 +12579,7 @@ HashMap.prototype = { * * Sometimes you want to get access to the injector of a currently running Angular app * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using extra `injector()` added + * application has been bootstrapped. You can do this using the extra `injector()` added * to JQuery/jqLite elements. See {@link angular.element}. * * *This is fairly rare but could be the case if a third party library is injecting the @@ -12956,20 +12614,39 @@ var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); -function annotate(fn) { + +function anonFn(fn) { + // For anonymous functions, showing at the very least the function signature can help in + // debugging. + var fnText = fn.toString().replace(STRIP_COMMENTS, ''), + args = fnText.match(FN_ARGS); + if (args) { + return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; + } + return 'fn'; +} + +function annotate(fn, strictDi, name) { var $inject, fnText, argDecl, last; - if (typeof fn == 'function') { + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { + if (strictDi) { + if (!isString(name) || !name) { + name = fn.name || anonFn(fn); + } + throw $injectorMinErr('strictdi', + '{0} is not using explicit annotation and cannot be invoked in strict mode', name); + } fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); @@ -12991,7 +12668,6 @@ function annotate(fn) { /** * @ngdoc service * @name $injector - * @function * * @description * @@ -13004,9 +12680,9 @@ function annotate(fn) { * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector){ + * expect($injector.invoke(function($injector) { * return $injector; - * }).toBe($injector); + * })).toBe($injector); * ``` * * # Injection Function Annotation @@ -13030,11 +12706,13 @@ function annotate(fn) { * ## Inference * * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with - * minification, and obfuscation tools since these tools change the argument names. + * can then be parsed and the function arguments can be extracted. This method of discovering + * annotations is disallowed when the injector is in strict mode. + * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the + * argument names. * * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. @@ -13048,6 +12726,7 @@ function annotate(fn) { * Return an instance of the service. * * @param {string} name The name of the instance to retrieve. + * @param {string} caller An optional string to provide the origin of the function call for error messages. * @return {*} The instance. */ @@ -13071,18 +12750,18 @@ function annotate(fn) { * @name $injector#has * * @description - * Allows the user to query if the particular service exist. + * Allows the user to query if the particular service exists. * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method * @name $injector#instantiate * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new - * operator and supplies all of the arguments to the constructor function as specified by the + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * * @param {Function} Type Annotated constructor function. @@ -13116,6 +12795,8 @@ function annotate(fn) { * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * + * You can disallow this method by using strict injection mode. + * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. * @@ -13168,6 +12849,8 @@ function annotate(fn) { * @param {Function|Array.<string|Function>} fn Function for which dependent service names need to * be retrieved as described above. * + * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. + * * @returns {Array.<string>} The names of the services which the function requires. */ @@ -13175,7 +12858,7 @@ function annotate(fn) { /** - * @ngdoc object + * @ngdoc service * @name $provide * * @description @@ -13477,11 +13160,12 @@ function annotate(fn) { */ -function createInjector(modulesToLoad) { +function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap(), + loadedModules = new HashMap([], true), providerCache = { $provide: { provider: supportObject(provider), @@ -13493,14 +13177,17 @@ function createInjector(modulesToLoad) { } }, providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function() { + createInternalInjector(providerCache, function(serviceName, caller) { + if (angular.isString(caller)) { + path.push(caller); + } throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(servicename) { - var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); + createInternalInjector(instanceCache, function(serviceName, caller) { + var provider = providerInjector.get(serviceName + providerSuffix, caller); + return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); })); @@ -13533,7 +13220,21 @@ function createInjector(modulesToLoad) { return providerCache[name + providerSuffix] = provider_; } - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + function enforceReturnValue(name, factory) { + return function enforcedReturnValue() { + var result = instanceInjector.invoke(factory, this); + if (isUndefined(result)) { + throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); + } + return result; + }; + } + + function factory(name, factoryFn, enforce) { + return provider(name, { + $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn + }); + } function service(name, constructor) { return factory(name, ['$injector', function($injector) { @@ -13541,7 +13242,7 @@ function createInjector(modulesToLoad) { }]); } - function value(name, val) { return factory(name, valueFn(val)); } + function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); @@ -13562,23 +13263,28 @@ function createInjector(modulesToLoad) { //////////////////////////////////// // Module Loading //////////////////////////////////// - function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; + function loadModules(modulesToLoad) { + var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); + function runInvokeQueue(queue) { + var i, ii; + for (i = 0, ii = queue.length; i < ii; i++) { + var invokeArgs = queue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } + try { if (isString(module)) { moduleFn = angularModule(module); runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { @@ -13611,17 +13317,18 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { - function getService(serviceName) { + function getService(serviceName, caller) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName); + return cache[serviceName] = factory(serviceName, caller); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -13633,13 +13340,18 @@ function createInjector(modulesToLoad) { } } - function invoke(fn, self, locals){ + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } + var args = [], - $inject = annotate(fn), + $inject = annotate(fn, strictDi, serviceName), length, i, key; - for(i = 0, length = $inject.length; i < length; i++) { + for (i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', @@ -13648,11 +13360,10 @@ function createInjector(modulesToLoad) { args.push( locals && locals.hasOwnProperty(key) ? locals[key] - : getService(key) + : getService(key, serviceName) ); } - if (!fn.$inject) { - // this means that we must be an array. + if (isArray(fn)) { fn = fn[length]; } @@ -13661,15 +13372,12 @@ function createInjector(modulesToLoad) { return fn.apply(self, args); } - function instantiate(Type, locals) { - var Constructor = function() {}, - instance, returnedValue; - + function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; - instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); + // Object creation: http://jsperf.com/create-constructor/2 + var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype); + var returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } @@ -13686,100 +13394,267 @@ function createInjector(modulesToLoad) { } } +createInjector.$$annotate = annotate; + /** - * @ngdoc service - * @name $anchorScroll - * @kind function - * @requires $window - * @requires $location - * @requires $rootScope + * @ngdoc provider + * @name $anchorScrollProvider * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, - * according to rules specified in - * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). - * - * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. - * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * - * @example - <example> - <file name="index.html"> - <div id="scrollArea" ng-controller="ScrollCtrl"> - <a ng-click="gotoBottom()">Go to bottom</a> - <a id="bottom"></a> You're at the bottom! - </div> - </file> - <file name="script.js"> - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - }; - } - </file> - <file name="style.css"> - #scrollArea { - height: 350px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - </file> - </example> + * Use `$anchorScrollProvider` to disable automatic scrolling whenever + * {@link ng.$location#hash $location.hash()} changes. */ function $AnchorScrollProvider() { var autoScrollingEnabled = true; + /** + * @ngdoc method + * @name $anchorScrollProvider#disableAutoScrolling + * + * @description + * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to + * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.<br /> + * Use this method to disable automatic scrolling. + * + * If automatic scrolling is disabled, one must explicitly call + * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the + * current hash. + */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; + /** + * @ngdoc service + * @name $anchorScroll + * @kind function + * @requires $window + * @requires $location + * @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). + * + * 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 + * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. + * + * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a + * vertical scroll-offset (either fixed or dynamic). + * + * @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. + * + * `yOffset` can be specified in various ways: + * - **number**: A fixed number of pixels to be used as offset.<br /><br /> + * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return + * a number representing the offset (in pixels).<br /><br /> + * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from + * the top of the page to the element's bottom will be used as offset.<br /> + * **Note**: The element will be taken into account only as long as its `position` is set to + * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust + * their height and/or positioning according to the viewport's size. + * + * <br /> + * <div class="alert alert-warning"> + * In order for `yOffset` to work properly, scrolling should take place on the document's root and + * not some child element. + * </div> + * + * @example + <example module="anchorScrollExample"> + <file name="index.html"> + <div id="scrollArea" ng-controller="ScrollController"> + <a ng-click="gotoBottom()">Go to bottom</a> + <a id="bottom"></a> You're at the bottom! + </div> + </file> + <file name="script.js"> + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function ($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + }]); + </file> + <file name="style.css"> + #scrollArea { + height: 280px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + </file> + </example> + * + * <hr /> + * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). + * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. + * + * @example + <example module="anchorScrollOffsetExample"> + <file name="index.html"> + <div class="fixed-header" ng-controller="headerCtrl"> + <a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]"> + Go to anchor {{x}} + </a> + </div> + <div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]"> + Anchor {{x}} of 5 + </div> + </file> + <file name="script.js"> + angular.module('anchorScrollOffsetExample', []) + .run(['$anchorScroll', function($anchorScroll) { + $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels + }]) + .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', + function ($anchorScroll, $location, $scope) { + $scope.gotoAnchor = function(x) { + var newHash = 'anchor' + x; + if ($location.hash() !== newHash) { + // set the $location.hash to `newHash` and + // $anchorScroll will automatically scroll to it + $location.hash('anchor' + x); + } else { + // call $anchorScroll() explicitly, + // since $location.hash hasn't changed + $anchorScroll(); + } + }; + } + ]); + </file> + <file name="style.css"> + body { + padding-top: 50px; + } + + .anchor { + border: 2px dashed DarkOrchid; + padding: 10px 10px 200px 10px; + } + + .fixed-header { + background-color: rgba(0, 0, 0, 0.2); + height: 50px; + position: fixed; + top: 0; left: 0; right: 0; + } + + .fixed-header > a { + display: inline-block; + margin: 5px 15px; + } + </file> + </example> + */ this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { var document = $window.document; - // helper function to get first anchor from a NodeList - // can't use filter.filter, as it accepts only instances of Array - // and IE can't convert NodeList to an array using [].slice - // TODO(vojta): use filter if we change it to accept lists as well + // Helper function to get first anchor from a NodeList + // (using `Array#some()` instead of `angular#forEach()` since it's more performant + // and working in all supported browsers.) function getFirstAnchor(list) { var result = null; - forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + Array.prototype.some.call(list, function(element) { + if (nodeName_(element) === 'a') { + result = element; + return true; + } }); return result; } + function getYOffset() { + + var offset = scroll.yOffset; + + if (isFunction(offset)) { + offset = offset(); + } else if (isElement(offset)) { + var elem = offset[0]; + var style = $window.getComputedStyle(elem); + if (style.position !== 'fixed') { + offset = 0; + } else { + offset = elem.getBoundingClientRect().bottom; + } + } else if (!isNumber(offset)) { + offset = 0; + } + + return offset; + } + + function scrollTo(elem) { + if (elem) { + elem.scrollIntoView(); + + var offset = getYOffset(); + + if (offset) { + // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. + // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the + // top of the viewport. + // + // IF the number of pixels from the top of `elem` to the end of the page's content is less + // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some + // way down the page. + // + // This is often the case for elements near the bottom of the page. + // + // In such cases we do not need to scroll the whole `offset` up, just the difference between + // the top of the element and the offset, which is enough to align the top of `elem` at the + // desired position. + var elemTop = elem.getBoundingClientRect().top; + $window.scrollBy(0, elemTop - offset); + } + } else { + $window.scrollTo(0, 0); + } + } + function scroll() { var hash = $location.hash(), elm; // empty hash, scroll to the top of the page - if (!hash) $window.scrollTo(0, 0); + if (!hash) scrollTo(null); // element with given id - else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + else if ((elm = document.getElementById(hash))) scrollTo(elm); // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') $window.scrollTo(0, 0); + else if (hash === 'top') scrollTo(null); } // does not scroll when user clicks on anchor link that is currently on // (no url change, no $location.hash() change), browser native does scroll if (autoScrollingEnabled) { $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { - $rootScope.$evalAsync(scroll); + function autoScrollWatchAction(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + + jqLiteDocumentLoaded(function() { + $rootScope.$evalAsync(scroll); + }); }); } @@ -13862,16 +13737,81 @@ var $AnimateProvider = ['$provide', function($provide) { * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { - if(arguments.length === 1) { + if (arguments.length === 1) { this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; } return this.$$classNameFilter; }; - this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { + + var currentDefer; + + function runAnimationPostDigest(fn) { + var cancelFn, defer = $$q.defer(); + defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { + cancelFn && cancelFn(); + }; + + $rootScope.$$postDigest(function ngAnimatePostDigest() { + cancelFn = fn(function ngAnimateNotifyComplete() { + defer.resolve(); + }); + }); + + return defer.promise; + } + + function resolveElementClasses(element, classes) { + var toAdd = [], toRemove = []; + + var hasClasses = createMap(); + forEach((element.attr('class') || '').split(/\s+/), function(className) { + hasClasses[className] = true; + }); + + forEach(classes, function(status, className) { + var hasClass = hasClasses[className]; + + // If the most recent class manipulation (via $animate) was to remove the class, and the + // element currently has the class, the class is scheduled for removal. Otherwise, if + // the most recent class manipulation (via $animate) was to add the class, and the + // element does not currently have the class, the class is scheduled to be added. + if (status === false && hasClass) { + toRemove.push(className); + } else if (status === true && !hasClass) { + toAdd.push(className); + } + }); + + return (toAdd.length + toRemove.length) > 0 && + [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null]; + } + + function cachedClassManipulation(cache, classes, op) { + for (var i=0, ii = classes.length; i < ii; ++i) { + var className = classes[i]; + cache[className] = op; + } + } + + function asyncPromise() { + // only serve one instance of a promise in order to save CPU cycles + if (!currentDefer) { + currentDefer = $$q.defer(); + $$asyncCallback(function() { + currentDefer.resolve(); + currentDefer = null; + }); + } + return currentDefer.promise; + } - function async(fn) { - fn && $$asyncCallback(fn); + function applyStyles(element, options) { + if (angular.isObject(options)) { + var styles = extend(options.from || {}, options.to || {}); + element.css(styles); + } } /** @@ -13892,58 +13832,58 @@ var $AnimateProvider = ['$provide', function($provide) { * page}. */ return { + animate: function(element, from, to) { + applyStyles(element, { from: from, to: to }); + return asyncPromise(); + }, /** * * @ngdoc method * @name $animate#enter - * @function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @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. * @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 {Function=} done callback function that will be called after the element has been - * inserted into the DOM + * @param {object=} options an optional collection of styles that will be applied to the element. + * @return {Promise} the animation callback promise */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); - } - async(done); + enter: function(element, parent, after, options) { + applyStyles(element, options); + after ? after.after(element) + : parent.prepend(element); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#leave - * @function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). + * @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 {Function=} done callback function that will be called after the element has been - * removed from the DOM + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - leave : function(element, done) { + leave: function(element, options) { element.remove(); - async(done); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#move - * @function + * @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. Once complete, the - * done() callback will be fired (if provided). + * 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. * * @param {DOMElement} element the element which will be moved around within the * DOM @@ -13951,89 +13891,148 @@ var $AnimateProvider = ['$provide', function($provide) { * inserted into (if the after element is not present) * @param {DOMElement} after the sibling element where the element will be * positioned next to - * @param {Function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position + * @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, done) { + 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. - this.enter(element, parent, after, done); + return this.enter(element, parent, after, options); }, /** * * @ngdoc method * @name $animate#addClass - * @function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). + * @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 {Function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { + 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); }); - async(done); + applyStyles(element, options); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#removeClass - * @function + * @kind function * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). + * 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 {Function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { + 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); }); - async(done); + applyStyles(element, options); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#setClass - * @function + * @kind function * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will it's CSS classes changed + * 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 {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove, done) { - forEach(element, function (element) { - jqLiteAddClass(element, add); - jqLiteRemoveClass(element, remove); - }); - async(done); + setClass: function(element, add, remove, options) { + var self = this; + var STORAGE_KEY = '$$animateClasses'; + var createdCache = false; + element = jqLite(element); + + var cache = element.data(STORAGE_KEY); + if (!cache) { + cache = { + classes: {}, + options: options + }; + createdCache = true; + } else if (options && cache.options) { + cache.options = angular.extend(cache.options || {}, options); + } + + var classes = cache.classes; + + add = isArray(add) ? add : add.split(' '); + remove = isArray(remove) ? remove : remove.split(' '); + cachedClassManipulation(classes, add, true); + cachedClassManipulation(classes, remove, false); + + if (createdCache) { + cache.promise = runAnimationPostDigest(function(done) { + var cache = element.data(STORAGE_KEY); + element.removeData(STORAGE_KEY); + + // in the event that the element is removed before postDigest + // is run then the cache will be undefined and there will be + // no need anymore to add or remove and of the element classes + if (cache) { + var classes = resolveElementClasses(element, cache.classes); + if (classes) { + self.$$setClassImmediately(element, classes[0], classes[1], cache.options); + } + } + + done(); + }); + element.data(STORAGE_KEY, cache); + } + + return cache.promise; + }, + + $$setClassImmediately: function(element, add, remove, options) { + add && this.$$addClassImmediately(element, add); + remove && this.$$removeClassImmediately(element, remove); + applyStyles(element, options); + return asyncPromise(); }, - enabled : noop + enabled: noop, + cancel: noop }; }]; }]; -function $$AsyncCallbackProvider(){ +function $$AsyncCallbackProvider() { this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { return $$rAF.supported ? function(fn) { return $$rAF(fn); } @@ -14043,6 +14042,8 @@ function $$AsyncCallbackProvider(){ }]; } +/* global stripHash: true */ + /** * ! This is a private undocumented service ! * @@ -14061,8 +14062,7 @@ function $$AsyncCallbackProvider(){ /** * @param {object} window The global window object. * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. + * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer) { @@ -14093,7 +14093,7 @@ function Browser(window, document, $log, $sniffer) { } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { + while (outstandingRequestCallbacks.length) { try { outstandingRequestCallbacks.pop()(); } catch (e) { @@ -14104,6 +14104,11 @@ function Browser(window, document, $log, $sniffer) { } } + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index + 1); + } + /** * @private * Note: this method is used only by scenario runner @@ -14114,7 +14119,7 @@ function Browser(window, document, $log, $sniffer) { // 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(); }); + forEach(pollFns, function(pollFn) { pollFn(); }); if (outstandingRequestCount === 0) { callback(); @@ -14156,7 +14161,7 @@ function Browser(window, document, $log, $sniffer) { */ function startPoller(interval, setTimeout) { (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); + forEach(pollFns, function(pollFn) { pollFn(); }); pollTimeout = setTimeout(check, interval); })(); } @@ -14165,9 +14170,13 @@ function Browser(window, document, $log, $sniffer) { // URL API ////////////////////////////////////////////////////////////// - var lastBrowserUrl = location.href, + var cachedState, lastHistoryState, + lastBrowserUrl = location.href, baseElement = document.find('base'), - newLocation = null; + reloadLocation = null; + + cacheState(); + lastHistoryState = cachedState; /** * @name $browser#url @@ -14186,52 +14195,110 @@ function Browser(window, document, $log, $sniffer) { * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? + * @param {boolean=} replace Should new url replace current history record? + * @param {object=} state object to use with pushState/replaceState */ - self.url = function(url, replace) { + self.url = function(url, replace, state) { + // In modern browsers `history.state` is `null` by default; treating it separately + // from `undefined` would cause `$browser.url('/foo')` to change `history.state` + // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. + if (isUndefined(state)) { + state = null; + } + // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { - if (lastBrowserUrl == url) return; + var sameState = lastHistoryState === state; + + // Don't change anything if previous and current URLs and states match. This also prevents + // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. + // See https://github.com/angular/angular.js/commit/ffb2701 + if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { + return self; + } + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else { - history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); - } + lastHistoryState = state; + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if ($sniffer.history && (!sameBase || !sameState)) { + history[replace ? 'replaceState' : 'pushState'](state, '', url); + cacheState(); + // Do the assignment again so that those two variables are referentially identical. + lastHistoryState = cachedState; } else { - newLocation = url; + if (!sameBase) { + reloadLocation = url; + } if (replace) { location.replace(url); - } else { + } else if (!sameBase) { location.href = url; + } else { + location.hash = getHash(url); } } return self; // getter } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. + // - reloadLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + return reloadLocation || location.href.replace(/%27/g,"'"); } }; + /** + * @name $browser#state + * + * @description + * This method is a getter. + * + * Return history.state or null if history.state is undefined. + * + * @returns {object} state + */ + self.state = function() { + return cachedState; + }; + var urlChangeListeners = [], urlChangeInit = false; + function cacheStateAndFireUrlChange() { + cacheState(); + fireUrlChange(); + } + + // This variable should be used *only* inside the cacheState function. + var lastCachedState = null; + function cacheState() { + // This should be the only place in $browser where `history.state` is read. + cachedState = window.history.state; + cachedState = isUndefined(cachedState) ? null : cachedState; + + // Prevent callbacks fo fire twice if both hashchange & popstate were fired. + if (equals(cachedState, lastCachedState)) { + cachedState = lastCachedState; + } + lastCachedState = cachedState; + } + function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; + if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { + return; + } lastBrowserUrl = self.url(); + lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { - listener(self.url()); + listener(self.url(), cachedState); }); } @@ -14264,11 +14331,9 @@ function Browser(window, document, $log, $sniffer) { // changed by push/replaceState // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); + if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); + jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } @@ -14277,6 +14342,13 @@ function Browser(window, document, $log, $sniffer) { return callback; }; + /** + * 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. + */ + self.$$checkUrlChange = fireUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// @@ -14302,6 +14374,14 @@ function Browser(window, document, $log, $sniffer) { var lastCookieString = ''; var cookiePath = self.baseHref(); + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + /** * @name $browser#cookies * @@ -14323,16 +14403,15 @@ function Browser(window, document, $log, $sniffer) { * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { - /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(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: @@ -14340,8 +14419,8 @@ function Browser(window, document, $log, $sniffer) { // - 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 ("+ + $log.warn("Cookie '" + name + + "' possibly not set or overflowed because it was too large (" + cookieLength + " > 4096 bytes)!"); } } @@ -14356,12 +14435,12 @@ function Browser(window, document, $log, $sniffer) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); + 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] = unescape(cookie.substring(index + 1)); + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); } } } @@ -14419,9 +14498,9 @@ function Browser(window, document, $log, $sniffer) { } -function $BrowserProvider(){ +function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ + function($window, $log, $sniffer, $document) { return new Browser($window, $document, $log, $sniffer); }]; } @@ -14493,8 +14572,10 @@ function $BrowserProvider(){ $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - $scope.cache.put(key, value); - $scope.keys.push(key); + if ($scope.cache.get(key) === undefined) { + $scope.keys.push(key); + } + $scope.cache.put(key, value === undefined ? null : value); }; }]); </file> @@ -14567,7 +14648,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#put - * @function + * @kind function * * @description * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be @@ -14603,7 +14684,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#get - * @function + * @kind function * * @description * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. @@ -14627,7 +14708,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#remove - * @function + * @kind function * * @description * Removes an entry from the {@link $cacheFactory.Cache Cache} object. @@ -14655,7 +14736,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#removeAll - * @function + * @kind function * * @description * Clears the cache object of any entries. @@ -14671,7 +14752,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#destroy - * @function + * @kind function * * @description * Destroys the {@link $cacheFactory.Cache Cache} object entirely, @@ -14688,7 +14769,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#info - * @function + * @kind function * * @description * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. @@ -14743,7 +14824,7 @@ function $CacheFactoryProvider() { * @name $cacheFactory#info * * @description - * Get information about all the of the caches that have been created + * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ @@ -14793,7 +14874,8 @@ function $CacheFactoryProvider() { * ``` * * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be below the `ng-app` definition. + * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, + * element with ng-app attribute), otherwise the template will be ignored. * * Adding via the $templateCache service: * @@ -14844,7 +14926,7 @@ function $TemplateCacheProvider() { /** * @ngdoc service * @name $compile - * @function + * @kind function * * @description * Compiles an HTML string or DOM into a template and produces a template function, which @@ -14882,9 +14964,9 @@ function $TemplateCacheProvider() { * template: '<div></div>', // or // function(tElement, tAttrs) { ... }, * // or * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * replace: false, * transclude: false, * restrict: 'A', + * templateNamespace: 'html', * scope: false, * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, * controllerAs: 'stringAlias', @@ -14935,6 +15017,13 @@ function $TemplateCacheProvider() { * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * + * #### `multiElement` + * When this property is set to true, the HTML compiler will collect DOM nodes between + * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them + * together as the directive elements. It is recommended that this feature be used on directives + * which are not strictly behavioural (such as {@link ngClick}), and which + * do not manipulate or replace child nodes (such as {@link ngInclude}). + * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it * is necessary to specify the order in which the directives are applied. The `priority` is used @@ -14946,7 +15035,8 @@ function $TemplateCacheProvider() { * #### `terminal` * If set to true then the current `priority` will be the last set of directives * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). + * as the order of execution on same `priority` is undefined). Note that expressions + * 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 @@ -14979,19 +15069,25 @@ function $TemplateCacheProvider() { * 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. + * 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 and to the parent scope, this can be + * 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})`. * * + * #### `bindToController` + * When an isolate scope is used for a component (see above), and `controllerAs` is used, `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. * * #### `controller` * Controller constructor function. The controller is instantiated before the @@ -15002,9 +15098,18 @@ function $TemplateCacheProvider() { * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: + * `function([scope], cloneLinkingFn, futureParentElement)`. + * * `scope`: optional argument to override the scope. + * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. + * * `futureParentElement`: + * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. + * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. + * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) + * and when the `cloneLinkinFn` is passed, + * as those elements need to created and cloned in a special way when they are defined outside their + * usual containers (e.g. like `<svg>`). + * * See also the `directive.templateNamespace` property. * * * #### `require` @@ -15015,9 +15120,12 @@ function $TemplateCacheProvider() { * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. + * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass + * `null` to the `link` fn if not found. + * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` @@ -15028,54 +15136,84 @@ function $TemplateCacheProvider() { * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. + * declaration style. If omitted, the defaults (elements and attributes) are used. * - * * `E` - Element name: `<my-directive></my-directive>` + * * `E` - Element name (default): `<my-directive></my-directive>` * * `A` - Attribute (default): `<div my-directive="exp"></div>` * * `C` - Class: `<div class="my-directive: exp;"></div>` * * `M` - Comment: `<!-- directive: my-directive exp -->` * * + * #### `templateNamespace` + * String representing the document type used by the markup in the template. + * AngularJS needs this information as those elements need to be created and cloned + * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`. + * + * * `html` - All root nodes in the template are HTML. Root nodes may also be + * top-level elements such as `<svg>` or `<math>`. + * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`). + * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`). + * + * If no `templateNamespace` is specified, then the namespace is considered to be `html`. + * * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * HTML markup that may: + * * Replace the contents of the directive's element (default). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * Value may be: + * + * * A string. For example `<div red-on-hover>{{delete_str}}</div>`. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * * * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. + * This is similar to `template` but the template is loaded from the specified URL, asynchronously. + * + * Because template loading is asynchronous the compiler will suspend compilation of directives on that element + * for later when the template has been resolved. In the meantime it will continue to compile and link + * sibling and parent elements as though this element had not contained any directives. + * + * The compiler does not suspend the entire compilation to wait for templates to be loaded because this + * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the + * case when only one deeply nested directive has `templateUrl`. + * + * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link - * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. * * - * #### `replace` - * specify where the template should be inserted. Defaults to `false`. + * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) + * specify what the template should replace. Defaults to `false`. * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. * + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#template-expanding-directive + * Directives Guide} for an example. + * + * There are very few scenarios where element replacement is required for the application function, + * the main one being reusable custom components that are used within SVG contexts + * (because SVG doesn't work with custom elements in the DOM tree). * * #### `transclude` - * compile the content of the element and make it available to the directive. - * Typically used with {@link ng.directive:ngTransclude - * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. + * Extract the contents of the element where the directive appears and make it available to the directive. + * The contents are compiled and provided to the directive as a **transclusion function**. See the + * {@link $compile#transclusion Transclusion} section below. + * + * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element or the entire element: * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. * * * #### `compile` @@ -15085,11 +15223,7 @@ function $TemplateCacheProvider() { * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. Examples that require compile functions are - * directives that transform template DOM, such as {@link - * api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link ngRoute.directive:ngView ngView}. The - * compile function takes the following arguments. + * template transformation, it is not used often. The compile function takes the following arguments: * * * `tElement` - template element - The element where the directive has been declared. It is * safe to do template transformation on the element and child elements only. @@ -15158,10 +15292,9 @@ function $TemplateCacheProvider() { * the directives to use the controllers as a communication channel. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. - * + * This is the same as the `$transclude` + * parameter of directive controllers, see there for details. + * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * @@ -15170,9 +15303,130 @@ function $TemplateCacheProvider() { * * #### Post-linking function * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * Executed after the child elements are linked. + * + * Note that child elements that contain `templateUrl` directives will not have been compiled + * and linked since they are waiting for their template to load asynchronously and their own + * compilation and linking has been suspended until that occurs. + * + * It is safe to do DOM transformation in the post-linking function on elements that are not waiting + * for their async templates to be resolved. + * + * + * ### Transclusion + * + * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and + * copying them to another part of the DOM, while maintaining their connection to the original AngularJS + * scope from where they were taken. + * + * Transclusion is used (often with {@link ngTransclude}) to insert the + * original contents of a directive's element into a specified place in the template of the directive. + * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded + * content has access to the properties on the scope from which it was taken, even if the directive + * has isolated scope. + * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. + * + * This makes it possible for the widget to have private state for its template, while the transcluded + * content has access to its originating scope. + * + * <div class="alert alert-warning"> + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + * </div> + * + * #### Transclusion Functions + * + * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion + * function** to the directive's `link` function and `controller`. This transclusion function is a special + * **linking function** that will return the compiled contents linked to a new transclusion scope. + * + * <div class="alert alert-info"> + * If you are just using {@link ngTransclude} then you don't need to worry about this function, since + * ngTransclude will deal with it for us. + * </div> + * + * If you want to manually control the insertion and removal of the transcluded content in your directive + * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery + * object that contains the compiled DOM, which is linked to the correct transclusion scope. + * + * When you call a transclusion function you can pass in a **clone attach function**. This function accepts + * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded + * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * + * <div class="alert alert-info"> + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function + * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. + * </div> + * + * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone + * attach function**: + * + * ```js + * var transcludedContent, transclusionScope; + * + * $transclude(function(clone, scope) { + * element.append(clone); + * transcludedContent = clone; + * transclusionScope = scope; + * }); + * ``` + * + * Later, if you want to remove the transcluded content from your DOM then you should also destroy the + * associated transclusion scope: + * + * ```js + * transcludedContent.remove(); + * transclusionScope.$destroy(); + * ``` + * + * <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), + * then you are also responsible for calling `$destroy` on the transclusion scope. + * </div> + * + * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} + * automatically destroy their transluded clones as necessary so you do not need to worry about this if + * you are simply using {@link ngTransclude} to inject the transclusion into your directive. + * + * + * #### Transclusion Scopes + * + * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion + * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed + * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it + * was taken. + * + * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look + * like this: + * + * ```html + * <div ng-app> + * <div isolate> + * <div transclusion> + * </div> + * </div> + * </div> + * ``` + * + * The `$parent` scope hierarchy will look like this: + * + * ``` + * - $rootScope + * - isolate + * - transclusion + * ``` + * + * but the scopes will inherit prototypically from different scopes to their `$parent`. + * + * ``` + * - $rootScope + * - transclusion + * - isolate + * ``` + * * - * <a name="Attributes"></a> * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the @@ -15210,17 +15464,17 @@ function $TemplateCacheProvider() { * } * ``` * - * Below is an example using `$compileProvider`. + * ## Example * * <div class="alert alert-warning"> * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. * </div> * - <example module="compile"> + <example module="compileExample"> <file name="index.html"> <script> - angular.module('compile', [], function($compileProvider) { + angular.module('compileExample', [], function($compileProvider) { // configure new 'compile' directive by passing a directive // factory function. The factory function injects the '$compile' $compileProvider.directive('compile', function($compile) { @@ -15244,15 +15498,14 @@ function $TemplateCacheProvider() { } ); }; - }) - }); - - function Ctrl($scope) { + }); + }) + .controller('GreeterController', ['$scope', function($scope) { $scope.name = 'Angular'; $scope.html = 'Hello {{name}}'; - } + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="GreeterController"> <input ng-model="name"> <br> <textarea ng-model="html"></textarea> <br> <div compile="html"></div> @@ -15274,10 +15527,17 @@ 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. + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. + * + * <div class="alert alert-error"> + * **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. + * </div> + * * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) - * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template + * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. @@ -15289,6 +15549,19 @@ function $TemplateCacheProvider() { * * `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. * + * * `options` - An optional object hash with linking options. If `options` is provided, then the following + * keys may be used to control linking behavior: + * + * * `parentBoundTranscludeFn` - the transclude function made available to + * 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. + * * `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. + * * Calling the linking function returns the element of the template. It is either the original * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * @@ -15327,7 +15600,6 @@ var $compileMinErr = minErr('$compile'); /** * @ngdoc provider * @name $compileProvider - * @function * * @description */ @@ -15335,18 +15607,46 @@ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/; + COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/, + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), + REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // 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)$/; + function parseIsolateBindings(scope, directiveName) { + var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; + + var bindings = {}; + + forEach(scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP); + + if (!match) { + throw $compileMinErr('iscp', + "Invalid isolate scope definition for directive '{0}'." + + " Definition: {... {1}: '{2}' ...}", + directiveName, scopeName, definition); + } + + bindings[scopeName] = { + mode: match[1][0], + collection: match[2] === '*', + optional: match[3] === '?', + attrName: match[4] || scopeName + }; + }); + + return bindings; + } + /** * @ngdoc method * @name $compileProvider#directive - * @function + * @kind function * * @description * Register a new directive with the compiler. @@ -15379,7 +15679,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.restrict = directive.restrict || 'EA'; + if (isObject(directive.scope)) { + directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name); + } directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -15399,13 +15702,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compileProvider#aHrefSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` @@ -15429,7 +15732,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compileProvider#imgSrcSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe @@ -15455,25 +15758,82 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data property containing an array of the binding expressions + * + * You may want to disable this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if (isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; + this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - var Attributes = function(element, attr) { + var Attributes = function(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } + this.$$element = element; - this.$attr = attr || {}; }; Attributes.prototype = { + /** + * @ngdoc method + * @name $compile.directive.Attributes#$normalize + * @kind function + * + * @description + * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or + * `data-`) to its normalized, camelCase form. + * + * Also there is special case for Moz prefix starting with upper case letter. + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * @param {string} name Name to normalize + */ $normalize: directiveNormalize, /** * @ngdoc method * @name $compile.directive.Attributes#$addClass - * @function + * @kind function * * @description * Adds the CSS class value specified by the classVal parameter to the element. If animations @@ -15481,8 +15841,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * @param {string} classVal The className value that will be added to the element */ - $addClass : function(classVal) { - if(classVal && classVal.length > 0) { + $addClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.addClass(this.$$element, classVal); } }, @@ -15490,7 +15850,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$removeClass - * @function + * @kind function * * @description * Removes the CSS class value specified by the classVal parameter from the element. If @@ -15498,8 +15858,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * @param {string} classVal The className value that will be removed from the element */ - $removeClass : function(classVal) { - if(classVal && classVal.length > 0) { + $removeClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.removeClass(this.$$element, classVal); } }, @@ -15507,7 +15867,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$updateClass - * @function + * @kind function * * @description * Adds and removes the appropriate CSS class values to the element based on the difference @@ -15516,16 +15876,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} newClasses The current CSS className value * @param {string} oldClasses The former CSS className value */ - $updateClass : function(newClasses, oldClasses) { + $updateClass: function(newClasses, oldClasses) { var toAdd = tokenDifference(newClasses, oldClasses); - var toRemove = tokenDifference(oldClasses, newClasses); + if (toAdd && toAdd.length) { + $animate.addClass(this.$$element, toAdd); + } - if(toAdd.length === 0) { + var toRemove = tokenDifference(oldClasses, newClasses); + if (toRemove && toRemove.length) { $animate.removeClass(this.$$element, toRemove); - } else if(toRemove.length === 0) { - $animate.addClass(this.$$element, toAdd); - } else { - $animate.setClass(this.$$element, toAdd, toRemove); } }, @@ -15543,13 +15902,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), - normalizedVal, + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(node, key), + observer = key, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if (aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -15566,10 +15930,44 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && key === 'href') || + (nodeName === 'img' && key === 'src')) { + // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); + } else if (nodeName === 'img' && key === 'srcset') { + // sanitize img[srcset] values + var result = ""; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i = 0; i < nbrUrisWith2parts; i++) { + var innerIdx = i * 2; + // sanitize the uri + result += $$sanitizeUri(trim(rawUris[innerIdx]), true); + // add the descriptor + result += (" " + trim(rawUris[innerIdx + 1])); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i * 2]).split(/\s/); + + // sanitize the last uri + result += $$sanitizeUri(trim(lastTuple[0]), true); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (" " + trim(lastTuple[1])); + } + this[key] = value = result; } if (writeAttr !== false) { @@ -15582,7 +15980,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { + $$observers && forEach($$observers[observer], function(fn) { try { fn(value); } catch (e) { @@ -15595,7 +15993,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$observe - * @function + * @kind function * * @description * Observes an interpolated attribute. @@ -15607,25 +16005,39 @@ 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#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), + $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter) { + if (!listeners.$$inter && attrs.hasOwnProperty(key)) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch (e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') @@ -15635,6 +16047,30 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; + + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + + $element.data('$binding', bindings); + } : noop; + + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + $element.data(dataName, scope); + } : noop; + + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; return compile; @@ -15649,48 +16085,74 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // 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 == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; + 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]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + compile.$$addScopeClass($compileNodes); + var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, options) { assertArg(scope, 'scope'); - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; - - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i<ii; i++) { - var node = $linkNode[i], - nodeType = node.nodeType; - if (nodeType === 1 /* element */ || nodeType === 9 /* document */) { - $linkNode.eq(i).data('$scope', scope); + options = options || {}; + var parentBoundTranscludeFn = options.parentBoundTranscludeFn, + transcludeControllers = options.transcludeControllers, + futureParentElement = options.futureParentElement; + + // When `parentBoundTranscludeFn` is passed, it is a + // `controllersBoundTransclude` function (it was previously passed + // as `transclude` to directive.link) so we must unwrap it to get + // its `boundTranscludeFn` + if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) { + parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude; + } + + if (!namespace) { + namespace = detectNamespaceForChildElements(futureParentElement); + } + var $linkNode; + if (namespace !== 'html') { + // When using a directive with replace:true and templateUrl the $compileNodes + // (or a child element inside of them) + // might change, so we need to recreate the namespace adapted compileNodes + // for call to the link function. + // Note: This will already clone the nodes... + $linkNode = jqLite( + wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html()) + ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; + } + + if (transcludeControllers) { + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } + compile.$$addScopeInfo($linkNode, scope); + if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. + function detectNamespaceForChildElements(parentElement) { + // TODO: Make this detect MathML as well... + var node = parentElement && parentElement[0]; + if (!node) { + return 'html'; + } else { + return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; } } @@ -15712,7 +16174,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); @@ -15727,7 +16189,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || @@ -15735,10 +16197,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { !childNodes.length) ? null : compileNodes(childNodes, - nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); + nodeLinkFn ? ( + (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) + && nodeLinkFn.transclude) : transcludeFn); + + if (nodeLinkFn || childLinkFn) { + linkFns.push(i, nodeLinkFn, childLinkFn); + linkFnFound = true; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + } - linkFns.push(nodeLinkFn, childLinkFn); - linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -15746,60 +16214,80 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; - function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n; + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, - stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; + stableNodeList = new Array(nodeListLength); + + // create a sparse array by only copying the elements which have a linkFn + for (i = 0; i < linkFns.length; i+=3) { + idx = linkFns[i]; + stableNodeList[idx] = nodeList[idx]; + } + } else { + stableNodeList = nodeList; } - for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { - node = stableNodeList[n]; + for (i = 0, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } - childTranscludeFn = nodeLinkFn.transclude; - if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, - createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) - ); + + if (nodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn, + nodeLinkFn.elementTranscludeOnThisElement); + + } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { + childBoundTranscludeFn = parentBoundTranscludeFn; + + } else if (!parentBoundTranscludeFn && transcludeFn) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); + } else { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn); + childBoundTranscludeFn = null; } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } - function createBoundTranscludeFn(scope, transcludeFn) { - return function boundTranscludeFn(transcludedScope, cloneFn, controllers) { - var scopeCreated = false; + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { + + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { - transcludedScope = scope.$new(); + transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; - scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers); - if (scopeCreated) { - clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy)); - } - return clone; + return transcludeFn(transcludedScope, cloneFn, { + parentBoundTranscludeFn: previousBoundTranscludeFn, + transcludeControllers: controllers, + futureParentElement: futureParentElement + }); }; + + return boundTranscludeFn; } /** @@ -15818,44 +16306,51 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { match, className; - switch(nodeType) { - case 1: /* Element */ + switch (nodeType) { + case NODE_TYPE_ELEMENT: /* Element */ // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } + name = attr.name; + value = trim(attr.value); + + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + name = name.replace(PREFIX_REGEXP, '') + .substr(8).replace(/_(.)/g, function(match, letter) { + return letter.toUpperCase(); + }); + } - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + 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); } + } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true - } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); } // use class as directive @@ -15870,10 +16365,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } break; - case 3: /* Text Node */ + case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; - case 8: /* Comment */ + case NODE_TYPE_COMMENT: /* Comment */ try { match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { @@ -15906,14 +16401,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodes = []; var depth = 0; if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - var startNode = node; do { if (!node) { throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); } - if (node.nodeType == 1 /** Element **/) { + if (node.nodeType == NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -15973,10 +16467,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var terminalPriority = -Number.MAX_VALUE, newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, + controllers, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, hasTranscludeDirective = false, + hasTemplate = false, hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, @@ -15988,7 +16484,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveValue; // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { + for (var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; var attrStart = directive.$$start; var attrEnd = directive.$$end; @@ -16004,17 +16500,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); if (isObject(directiveValue)) { + // This directive is trying to add an isolated scope. + // Check that there is no scope of any kind already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, + directive, $compileNode); newIsolateScopeDirective = directive; + } else { + // This directive is trying to add a child scope. + // Check that there is no isolated scope already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); } } + + newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; @@ -16041,12 +16545,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { @@ -16067,6 +16571,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.template) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -16081,11 +16586,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(directiveValue); + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, ''); @@ -16116,6 +16621,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -16124,7 +16630,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, @@ -16152,7 +16658,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present @@ -16164,6 +16674,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (pre) { if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; + pre.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { pre = cloneAndAnnotateFn(pre, {isolateScope: true}); } @@ -16172,6 +16683,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (post) { if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; + post.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { post = cloneAndAnnotateFn(post, {isolateScope: true}); } @@ -16180,33 +16692,47 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - function getControllers(require, $element, elementControllers) { + function getControllers(directiveName, require, $element, elementControllers) { var value, retrievalMethod = 'data', optional = false; + var $searchElement = $element; + var match; if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; + match = require.match(REQUIRE_PREFIX_REGEXP); + require = require.substring(match[0].length); + + 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; + } + value = null; if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; + if (value = elementControllers[require]) { + value = value.instance; + } } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); + value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } - return value; + return value || null; } else if (isArray(require)) { value = []; forEach(require, function(require) { - value.push(getControllers(require, $element, elementControllers)); + value.push(getControllers(directiveName, require, $element, elementControllers)); }); } return value; @@ -16214,52 +16740,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, + attrs; if (compileNode === linkNode) { attrs = templateAttrs; + $element = templateAttrs.$$element; } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + $element = jqLite(linkNode); + attrs = new Attributes($element, templateAttrs); } - $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - isolateScope = scope.$new(true); + } - if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); - } + if (boundTranscludeFn) { + // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` + // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` + transcludeFn = controllersBoundTransclude; + transcludeFn.$$boundTransclude = boundTranscludeFn; + } + + 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; + }); + } - safeAddClass($linkNode, 'ng-isolate-scope'); + if (newIsolateScopeDirective) { + 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; + } - forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - optional = (match[2] == '?'), - mode = match[1], // @, =, or & + forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) { + var attrName = definition.attrName, + optional = definition.optional, + mode = definition.mode, // @, =, or & lastValue, parentGet, parentSet, compare; - isolateScope.$$isolateBindings[scopeName] = mode + attrName; - switch (mode) { case '@': attrs.$observe(attrName, function(value) { - isolateScope[scopeName] = value; + isolateBindingContext[scopeName] = value; }); attrs.$$observers[attrName].$$scope = scope; - if( attrs[attrName] ) { + 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 - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); + isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); } break; @@ -16271,88 +16836,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a,b) { return a === b; }; + 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 = isolateScope[scopeName] = parentGet(scope); + lastValue = isolateBindingContext[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", attrs[attrName], newIsolateScopeDirective.name); }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { + 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 - isolateScope[scopeName] = parentValue; + isolateBindingContext[scopeName] = parentValue; } else { // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); + parentSet(scope, parentValue = isolateBindingContext[scopeName]); } } return lastValue = parentValue; - }, null, parentGet.literal); + }; + 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; case '&': parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { + isolateBindingContext[scopeName] = function(locals) { return parentGet(scope, locals); }; break; - - default: - throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - newIsolateScopeDirective.name, scopeName, definition); } }); } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - 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); - // 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); - } - - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; - } + if (controllers) { + forEach(controllers, function(controller) { + controller(); }); + controllers = null; } // PRELINKING - for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = 0, ii = preLinkFns.length; i < ii; i++) { + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -16365,22 +16907,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); // POSTLINKING - for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = postLinkFns.length - 1; i >= 0; i--) { + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { + // Note: all arguments are optional! + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { var transcludeControllers; - // no scope passed - if (arguments.length < 2) { + // No scope passed in: + if (!isScope(scope)) { + futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } @@ -16388,8 +16933,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } - - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + if (!futureParentElement) { + futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; + } + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } @@ -16420,11 +16967,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (name === ignoreDirective) return null; var match = null; if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i<ii; i++) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { try { directive = directives[i]; - if ( (maxPriority === undefined || maxPriority > directive.priority) && + if ((maxPriority === undefined || maxPriority > directive.priority) && directive.restrict.indexOf(location) != -1) { if (startAttrName) { directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); @@ -16432,7 +16979,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { tDirectives.push(directive); match = directive; } - } catch(e) { $exceptionHandler(e); } + } catch (e) { $exceptionHandler(e); } } } return match; @@ -16440,6 +16987,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. * The desired effect is to have both of the attributes present. @@ -16455,7 +17023,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // reapply the old attributes to the new element forEach(dst, function(value, key) { if (key.charAt(0) != '$') { - if (src[key]) { + if (src[key] && src[key] !== value) { value += (key === 'style' ? ';' : ' ') + src[key]; } dst.$set(key, value, true, srcAttr[key]); @@ -16494,12 +17062,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + : origAsyncDirective.templateUrl, + templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { + $templateRequest($sce.getTrustedResourceUrl(templateUrl)) + .then(function(content) { var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); @@ -16508,11 +17077,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(content); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", origAsyncDirective.name, templateUrl); @@ -16544,14 +17113,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - - while(linkQueue.length) { + while (linkQueue.length) { var scope = linkQueue.shift(), beforeTemplateLinkNode = linkQueue.shift(), linkRootElement = linkQueue.shift(), boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; + if (scope.$$destroyed) continue; + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; @@ -16560,14 +17130,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); } - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); // Copy in CSS classes from original node safeAddClass(jqLite(linkNode), oldClasses); } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } else { childBoundTranscludeFn = boundTranscludeFn; } @@ -16575,19 +17144,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); + linkQueue.push(scope, + node, + rootElement, + childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } @@ -16617,20 +17188,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (interpolateFn) { directives.push({ priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) + compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); + compile.$$addBindingInfo(parent, interpolateFn.expressions); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } }); } } + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch (type) { + case 'svg': + case 'math': + var wrapper = document.createElement('div'); + wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>'; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } + + function getTrustedContext(node, attrNormalizedName) { if (attrNormalizedName == "srcdoc") { return $sce.HTML; @@ -16638,22 +17231,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var tag = nodeName_(node); // maction[xlink:href] can source SVG. It's not limited to <maction>. if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || + (tag == "form" && attrNormalizedName == "action") || + (tag != "img" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); + function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + var trustedContext = getTrustedContext(node, name); + allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + + var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - if (name === "multiple" && nodeName_(node) === "SELECT") { + if (name === "multiple" && nodeName_(node) === "select") { throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}", startingTag(node)); @@ -16672,17 +17268,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { "ng- versions (such as ng-click instead of onclick) instead."); } - // we need to interpolate again, in case the attribute value has been updated - // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); + // If the attribute has changed since last $interpolate()ed + var newValue = attr[name]; + if (newValue !== value) { + // we need to interpolate again since the attribute value has been updated + // (e.g. by another directive's compile function) + // ensure unset/empty values make interpolateFn falsy + interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); + value = newValue; + } // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { @@ -16692,7 +17296,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //skip animations when the first digest occurs (when //both the new and the old values are the same) since //the CSS classes are the non-interpolated values - if(name === 'class' && newValue != oldValue) { + if (name === 'class' && newValue != oldValue) { attr.$updateClass(newValue, oldValue); } else { attr.$set(name, newValue); @@ -16722,7 +17326,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { i, ii; if ($rootElement) { - for(i = 0, ii = $rootElement.length; i < ii; i++) { + for (i = 0, ii = $rootElement.length; i < ii; i++) { if ($rootElement[i] == firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, @@ -16735,6 +17339,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } $rootElement.length -= removeCount - 1; + + // If the replaced element is also the jQuery .context then replace it + // .context is a deprecated jQuery api, so we should set it only when jQuery set it + // http://api.jquery.com/context/ + if ($rootElement.context === firstElementToRemove) { + $rootElement.context = newNode; + } break; } } @@ -16743,9 +17354,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parent) { parent.replaceChild(newNode, firstElementToRemove); } + + // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + + // 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]); + } + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { var element = elementsToRemove[k]; jqLite(element).remove(); // must do this way to clean up expando @@ -16761,19 +17396,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } + + + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } }]; } -var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; /** * Converts all accepted directives format into proper directive name. - * All of these will become 'myDirective': - * my:Directive - * my-directive - * x-my-directive - * data-my:directive - * - * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function directiveNormalize(name) { @@ -16789,21 +17426,25 @@ function directiveNormalize(name) { * element attributes. The values reflect current binding state `{{ }}`. The normalization is * needed since all of these are treated as equivalent in Angular: * + * ``` * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a"> + * ``` */ /** * @ngdoc property * @name $compile.directive.Attributes#$attr - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. */ /** * @ngdoc method * @name $compile.directive.Attributes#$set - * @function + * @kind function * * @description * Set DOM element attribute value. @@ -16826,7 +17467,7 @@ function nodesetLinkingFn( /* NodeList */ nodeList, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn -){} +) {} function directiveLinkingFn( /* nodesetLinkingFn */ nodesetLinkingFn, @@ -16834,7 +17475,7 @@ function directiveLinkingFn( /* Node */ node, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn -){} +) {} function tokenDifference(str1, str2) { var values = '', @@ -16842,16 +17483,33 @@ function tokenDifference(str1, str2) { tokens2 = str2.split(/\s+/); outer: - for(var i = 0; i < tokens1.length; i++) { + for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; + for (var j = 0; j < tokens2.length; j++) { + if (token == tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } return values; } +function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === NODE_TYPE_COMMENT) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; +} + /** * @ngdoc provider * @name $controllerProvider @@ -16864,6 +17522,7 @@ function tokenDifference(str1, str2) { */ function $ControllerProvider() { var controllers = {}, + globals = false, CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; @@ -16884,6 +17543,15 @@ function $ControllerProvider() { } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { @@ -16898,7 +17566,12 @@ function $ControllerProvider() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. @@ -16909,34 +17582,78 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals) { + return function(expression, locals, later, ident) { + // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. + // param `ident` --- An optional label which overrides the label parsed from the controller + // expression, if any. var instance, match, constructor, identifier; + later = later === true; + if (ident && isString(ident)) { + identifier = ident; + } - if(isString(expression)) { + if (isString(expression)) { match = expression.match(CNTRL_REG), constructor = match[1], - identifier = match[3]; + identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); assertArgFn(expression, constructor, true); } - instance = $injector.instantiate(expression, locals); - - if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + // Object creation: http://jsperf.com/create-constructor/2 + var controllerPrototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = Object.create(controllerPrototype); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } - locals.$scope[identifier] = instance; + return extend(function() { + $injector.invoke(expression, instance, locals, constructor); + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + + instance = $injector.instantiate(expression, locals, constructor); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; + + function addIdentifier(locals, identifier, instance, name) { + if (!(locals && isObject(locals.$scope))) { + throw minErr('$controller')('noscp', + "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + name, identifier); + } + + locals.$scope[identifier] = instance; + } }]; } @@ -16949,23 +17666,24 @@ function $ControllerProvider() { * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - <example> + <example module="documentExample"> <file name="index.html"> - <div ng-controller="MainCtrl"> + <div ng-controller="ExampleController"> <p>$document title: <b ng-bind="title"></b></p> <p>window.document title: <b ng-bind="windowTitle"></b></p> </div> </file> <file name="script.js"> - function MainCtrl($scope, $document) { - $scope.title = $document[0].title; - $scope.windowTitle = angular.element(window.document)[0].title; - } + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); </file> </example> */ -function $DocumentProvider(){ - this.$get = ['$window', function(window){ +function $DocumentProvider() { + this.$get = ['$window', function(window) { return jqLite(window.document); }]; } @@ -16986,8 +17704,8 @@ function $DocumentProvider(){ * ## Example: * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { - * return function (exception, cause) { + * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { + * return function(exception, cause) { * exception.message += ' (caused by "' + cause + '")'; * throw exception; * }; @@ -16997,6 +17715,14 @@ function $DocumentProvider(){ * This example will override the normal action of `$exceptionHandler`, to make angular * exceptions fail hard when they happen, instead of just logging to the console. * + * <hr /> + * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` + * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} + * (unless executed during a digest). + * + * If you wish, you can manually delegate exceptions, e.g. + * `try { ... } catch(e) { $exceptionHandler(e); }` + * * @param {Error} exception Exception associated with the error. * @param {string=} cause optional information about the context in which * the error was thrown. @@ -17010,6 +17736,36 @@ function $ExceptionHandlerProvider() { }]; } +var APPLICATION_JSON = 'application/json'; +var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; +var JSON_START = /^\[|^\{(?!\{)/; +var JSON_ENDS = { + '[': /]$/, + '{': /}$/ +}; +var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; + +function defaultHttpResponseTransform(data, headers) { + if (isString(data)) { + // Strip json vulnerability protection prefix and trim whitespace + var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); + + if (tempData) { + var contentType = headers('Content-Type'); + if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { + data = fromJson(tempData); + } + } + } + + return data; +} + +function isJsonLike(str) { + var jsonStart = str.match(JSON_START); + return jsonStart && JSON_ENDS[jsonStart[0]].test(str); +} + /** * Parse headers into key value object * @@ -17017,7 +17773,7 @@ function $ExceptionHandlerProvider() { * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { - var parsed = {}, key, val, i; + var parsed = createMap(), key, val, i; if (!headers) return parsed; @@ -17027,11 +17783,7 @@ function parseHeaders(headers) { val = trim(line.substr(i + 1)); if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); @@ -17058,7 +17810,11 @@ function headersGetter(headers) { if (!headersObj) headersObj = parseHeaders(headers); if (name) { - return headersObj[lowercase(name)] || null; + var value = headersObj[lowercase(name)]; + if (value === void 0) { + value = null; + } + return value; } return headersObj; @@ -17072,16 +17828,17 @@ function headersGetter(headers) { * This function is used for both request and response transforming * * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. + * @param {function(string=)} headers HTTP headers getter fn. + * @param {number} status HTTP status code of the response. * @param {(Function|Array.<Function>)} fns Function or an array of functions. * @returns {*} Transformed data. */ -function transformData(data, headers, fns) { +function transformData(data, headers, status, fns) { if (isFunction(fns)) - return fns(data, headers); + return fns(data, headers, status); forEach(fns, function(fn) { - data = fn(data, headers); + data = fn(data, headers, status); }); return data; @@ -17093,27 +17850,47 @@ function isSuccess(status) { } +/** + * @ngdoc provider + * @name $httpProvider + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - + /** + * @ngdoc property + * @name $httpProvider#defaults + * @description + * + * 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.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + * + **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data); - } - return data; - }], + transformResponse: [defaultHttpResponseTransform], // transform outgoing request data transformRequest: [function(d) { - return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; + return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; }], // default headers @@ -17121,26 +17898,57 @@ function $HttpProvider() { common: { 'Accept': 'application/json, text/plain, */*' }, - post: copy(CONTENT_TYPE_APPLICATION_JSON), - put: copy(CONTENT_TYPE_APPLICATION_JSON), - patch: copy(CONTENT_TYPE_APPLICATION_JSON) + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) }, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN' }; + var useApplyAsync = false; /** - * Are ordered by request, i.e. they are applied in the same order as the - * array, on request, but reverse order, on response. - */ - var interceptorFactories = this.interceptors = []; + * @ngdoc method + * @name $httpProvider#useApplyAsync + * @description + * + * Configure $http service to combine processing of multiple http responses received at around + * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in + * 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. + * + * @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 + * to load and share the same digest cycle. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useApplyAsync = function(value) { + if (isDefined(value)) { + useApplyAsync = !!value; + return this; + } + return useApplyAsync; + }; /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; + * @ngdoc property + * @name $httpProvider#interceptors + * @description + * + * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} + * pre-processing of request or postprocessing of responses. + * + * These service factories are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. + * + * {@link ng.$http#interceptors Interceptors detailed info} + **/ + var interceptorFactories = this.interceptors = []; this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { @@ -17159,27 +17967,6 @@ function $HttpProvider() { ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - /** * @ngdoc service * @kind function @@ -17206,13 +17993,14 @@ function $HttpProvider() { * it is important to familiarize yourself with these APIs and the guarantees they provide. * * - * # General usage + * ## 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 - * $http({method: 'GET', url: '/someUrl'}). + * // Simple GET request example : + * $http.get('/someUrl'). * success(function(data, status, headers, config) { * // this callback will be called asynchronously * // when the response is available @@ -17223,6 +18011,20 @@ function $HttpProvider() { * }); * ``` * + * ```js + * // Simple POST request example (passing data) : + * $http.post('/someUrl', {msg:'hello word!'}). + * 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. + * }); + * ``` + * + * * 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 @@ -17233,7 +18035,7 @@ function $HttpProvider() { * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * - * # Writing Unit Tests that use $http + * ## 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. @@ -17244,7 +18046,7 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * # Shortcut methods + * ## 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. @@ -17262,9 +18064,10 @@ function $HttpProvider() { * - {@link ng.$http#put $http.put} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} * * - * # Setting HTTP Headers + * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration @@ -17294,37 +18097,85 @@ function $HttpProvider() { * In addition, you can supply a `headers` property in the config object passed when * calling `$http(config)`, which overrides the defaults without changing them globally. * + * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, + * Use the `headers` property, setting the desired header to `undefined`. For example: + * + * ```js + * var req = { + * method: 'POST', + * url: 'http://example.com', + * headers: { + * 'Content-Type': undefined + * }, + * data: { test: 'test' }, + * } * - * # Transforming Requests and Responses + * $http(req).success(function(){...}).error(function(){...}); + * ``` * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: + * ## Transforming Requests and Responses * - * Request transformations: + * Both requests and responses can be transformed using transformation functions: `transformRequest` + * and `transformResponse`. These properties can be a single function that returns + * 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. + * + * ### Default Transformations + * + * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and + * `defaults.transformResponse` properties. If a request does not provide its own transformations + * then these will be applied. + * + * You can augment or replace the default transformations by modifying these properties by adding to or + * replacing the array. + * + * Angular provides the following default transformations: + * + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations: + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): * * - If XSRF prefix is detected, strip it (see Security Considerations section below). * - If JSON response is detected, deserialize it using a JSON parser. * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * ### Overriding the Default Transformations Per Request + * + * If you wish override the request/response transformations only for a single request then provide + * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * + * Note that if you provide these properties on the config object the default transformations will be + * overwritten. If you wish to augment the default transformations then you must include them in your + * local transformation array. + * + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. + * + * ```js + * function appendTransform(defaults, transform) { * - * # Caching + * // We can't guarantee that the default transformation is an array + * defaults = angular.isArray(defaults) ? defaults : [defaults]; + * + * // Append the new transformation to the defaults + * return defaults.concat(transform); + * } + * + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` + * + * + * ## 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`}). @@ -17341,13 +18192,13 @@ function $HttpProvider() { * * You can change the default cache to a new object (built with * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set + * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set * their `cache` property to `true` will now use this cache object. * * If you set the default cache to `false` then only requests that specify their own custom * cache object will be cached. * - * # Interceptors + * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -17365,14 +18216,14 @@ function $HttpProvider() { * * There are two kinds of interceptors (and two kinds of rejection interceptors): * - * * `request`: interceptors get called with http `config` object. The function is free to - * modify the `config` or create a new one. The function needs to return the `config` - * directly or as a promise. + * * `request`: interceptors get called with a http `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 * resolved with a rejection. * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` or create a new one. The function needs to return the `response` - * directly or as a promise. + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. * * `responseError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * @@ -17384,7 +18235,7 @@ function $HttpProvider() { * // optional method * 'request': function(config) { * // do something on success - * return config || $q.when(config); + * return config; * }, * * // optional method @@ -17401,7 +18252,7 @@ function $HttpProvider() { * // optional method * 'response': function(response) { * // do something on success - * return response || $q.when(response); + * return response; * }, * * // optional method @@ -17432,52 +18283,7 @@ function $HttpProvider() { * }); * ``` * - * # Response interceptors (DEPRECATED) - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - * ```js - * // register the interceptor as a service - * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return function(promise) { - * return promise.then(function(response) { - * // do something on success - * return response; - * }, function(response) { - * // do something on error - * if (canRecover(response)) { - * return responseOrNewPromise - * } - * return $q.reject(response); - * }); - * } - * }); - * - * $httpProvider.responseInterceptors.push('myHttpInterceptor'); - * - * - * // register the interceptor via an anonymous factory - * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { - * return function(promise) { - * // same as above - * } - * }); - * ``` - * - * - * # Security Considerations + * ## Security Considerations * * When designing web applications, consider security threats from: * @@ -17488,7 +18294,7 @@ function $HttpProvider() { * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * - * ## JSON Vulnerability Protection + * ### JSON Vulnerability Protection * * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * allows third party website to turn your JSON resource URL into @@ -17510,7 +18316,7 @@ function $HttpProvider() { * Angular will strip the prefix, before processing the JSON. * * - * ## Cross Site Request Forgery (XSRF) Protection + * ### 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 @@ -17526,7 +18332,7 @@ function $HttpProvider() { * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName @@ -17552,18 +18358,22 @@ function $HttpProvider() { * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} * - **transformResponse** – - * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – + * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` – * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. + * 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. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See [requests with credentials]https://developer.mozilla.org/en/http_access_control#section_5 + * - **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). @@ -17588,9 +18398,9 @@ function $HttpProvider() { * * * @example -<example> +<example module="httpExample"> <file name="index.html"> - <div ng-controller="FetchCtrl"> + <div ng-controller="FetchController"> <select ng-model="method"> <option>GET</option> <option>JSONP</option> @@ -17600,11 +18410,11 @@ function $HttpProvider() { <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> <button id="samplejsonpbtn" ng-click="updateModel('JSONP', - 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> + 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> Sample JSONP </button> <button id="invalidjsonpbtn" - ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> + ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> Invalid JSONP </button> <pre>http status code: {{status}}</pre> @@ -17612,30 +18422,32 @@ function $HttpProvider() { </div> </file> <file name="script.js"> - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $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; - }); - }; + angular.module('httpExample', []) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $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; + }); + }; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); </file> <file name="http-hello.html"> Hello, $http! @@ -17655,12 +18467,13 @@ function $HttpProvider() { expect(data.getText()).toMatch(/Hello, \$http!/); }); - it('should make a JSONP request to angularjs.org', function() { - sampleJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Super Hero!/); - }); +// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 +// it('should make a JSONP request to angularjs.org', function() { +// sampleJsonpBtn.click(); +// fetchBtn.click(); +// expect(status.getText()).toMatch('200'); +// expect(data.getText()).toMatch(/Super Hero!/); +// }); it('should make JSONP request to invalid URL and invoke the error handler', function() { @@ -17673,31 +18486,26 @@ function $HttpProvider() { </example> */ function $http(requestConfig) { - var config = { + + if (!angular.isObject(requestConfig)) { + throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); + } + + var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse - }; - var headers = mergeHeaders(requestConfig); + }, requestConfig); - extend(config, requestConfig); - config.headers = headers; + config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); - var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; - } - - var serverRequest = function(config) { - headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); + var headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); // strip content-type if data is undefined - if (isUndefined(config.data)) { + if (isUndefined(reqData)) { forEach(headers, function(value, header) { if (lowercase(header) === 'content-type') { delete headers[header]; @@ -17710,7 +18518,7 @@ function $HttpProvider() { } // send request - return sendReq(config, reqData, headers).then(transformResponse, transformResponse); + return sendReq(config, reqData).then(transformResponse, transformResponse); }; var chain = [serverRequest, undefined]; @@ -17726,7 +18534,7 @@ function $HttpProvider() { } }); - while(chain.length) { + while (chain.length) { var thenFn = chain.shift(); var rejectFn = chain.shift(); @@ -17751,14 +18559,34 @@ function $HttpProvider() { function transformResponse(response) { // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, config.transformResponse) - }); + var resp = extend({}, response); + if (!response.data) { + resp.data = response.data; + } else { + resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); + } return (isSuccess(response.status)) ? resp : $q.reject(resp); } + function executeHeaderFns(headers) { + var headerContent, processedHeaders = {}; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(); + if (headerContent != null) { + processedHeaders[header] = headerContent; + } + } else { + processedHeaders[header] = headerFn; + } + }); + + return processedHeaders; + } + function mergeHeaders(config) { var defHeaders = defaults.headers, reqHeaders = extend({}, config.headers), @@ -17766,10 +18594,6 @@ function $HttpProvider() { defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - // using for-in instead of forEach to avoid unecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { @@ -17784,22 +18608,8 @@ function $HttpProvider() { reqHeaders[defHeaderName] = defHeaders[defHeaderName]; } - return reqHeaders; - - function execHeaders(headers) { - var headerContent; - - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(); - if (headerContent != null) { - headers[header] = headerContent; - } else { - delete headers[header]; - } - } - }); - } + // execute if header value is a function for merged headers + return executeHeaderFns(reqHeaders); } } @@ -17849,7 +18659,7 @@ function $HttpProvider() { * Shortcut method to perform `JSONP` request. * * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. + * The name of the callback should be the string `JSON_CALLBACK`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -17880,7 +18690,20 @@ function $HttpProvider() { * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property @@ -17929,18 +18752,20 @@ function $HttpProvider() { * !!! ACCESSES CLOSURE VARS: * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests */ - function sendReq(config, reqData, reqHeaders) { + function sendReq(config, reqData) { var deferred = $q.defer(), promise = deferred.promise, cache, cachedResp, + reqHeaders = config.headers, url = buildUrl(config.url, config.params); $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache : isObject(defaults.cache) ? defaults.cache : defaultCache; @@ -17949,14 +18774,13 @@ function $HttpProvider() { if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; + cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]), cachedResp[3]); + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); } else { resolvePromise(cachedResp, 200, {}, 'OK'); } @@ -17967,8 +18791,17 @@ function $HttpProvider() { } } - // if we won't have the response in cache, send the request to the backend + + // if we won't have the response in cache, set the xsrf headers and + // send the request to the backend if (isUndefined(cachedResp)) { + var xsrfValue = urlIsSameOrigin(config.url) + ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, config.withCredentials, config.responseType); } @@ -17992,8 +18825,16 @@ function $HttpProvider() { } } - resolvePromise(response, status, headersString, statusText); - if (!$rootScope.$$phase) $rootScope.$apply(); + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } } @@ -18009,55 +18850,50 @@ function $HttpProvider() { status: status, headers: headersGetter(headers), config: config, - statusText : statusText + statusText: statusText }); } + function resolvePromiseWithResult(result) { + resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText); + } function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); + var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } 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)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if(parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + 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); + } } - return url; - } - - + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if (parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } }]; } -function createXhr(method) { - //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest - //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest - //if it is available - if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || - !window.XMLHttpRequest)) { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } else if (window.XMLHttpRequest) { - return new window.XMLHttpRequest(); - } - - throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); +function createXhr() { + return new window.XMLHttpRequest(); } /** @@ -18083,11 +18919,8 @@ function $HttpBackendProvider() { } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); @@ -18095,20 +18928,17 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc var callbackId = '_' + (callbacks.counter++).toString(36); callbacks[callbackId] = function(data) { callbacks[callbackId].data = data; + callbacks[callbackId].called = true; }; var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; + callbackId, function(status, text) { + completeRequest(callback, status, callbacks[callbackId].data, "", text); + callbacks[callbackId] = noop; }); } else { - var xhr = createXhr(method); + var xhr = createXhr(); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -18117,37 +18947,39 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }); - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and - // Safari respectively. - if (xhr && xhr.readyState == 4) { - var responseHeaders = null, - response = null; - - if(status !== ABORTED) { - responseHeaders = xhr.getAllResponseHeaders(); - - // responseText is the old-school way of retrieving response (supported by IE8 & 9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - response = ('response' in xhr) ? xhr.response : xhr.responseText; - } + xhr.onload = function requestLoaded() { + var statusText = xhr.statusText || ''; + + // responseText is the old-school way of retrieving response (supported by IE8 & 9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + var response = ('response' in xhr) ? xhr.response : xhr.responseText; - completeRequest(callback, - status || xhr.status, - response, - responseHeaders, - xhr.statusText || ''); + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; + + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; } + + completeRequest(callback, + status, + response, + xhr.getAllResponseHeaders(), + statusText); }; + var requestError = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, ''); + }; + + xhr.onerror = requestError; + xhr.onabort = requestError; + if (withCredentials) { xhr.withCredentials = true; } @@ -18174,66 +19006,62 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { - status = ABORTED; jsonpDone && jsonpDone(); xhr && xhr.abort(); } function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution - timeoutId && $browserDefer.cancel(timeoutId); - jsonpDone = xhr = null; - - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources or on Android 4.1 stock browser - // while retrieving files from application cache. - if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + if (timeoutId !== undefined) { + $browserDefer.cancel(timeoutId); } - - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status === 1223 ? 204 : status; - statusText = statusText || ''; + jsonpDone = xhr = null; callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; - function jsonpReq(url, done) { + function jsonpReq(url, callbackId, done) { // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - - script.type = 'text/javascript'; + var script = rawDocument.createElement('script'), callback = null; + script.type = "text/javascript"; script.src = url; - - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, "load", callback); + removeEventListenerFn(script, "error", callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = "unknown"; + + if (event) { + if (event.type === "load" && !callbacks[callbackId].called) { + event = { type: "error" }; } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } + text = event.type; + status = event.type === "error" ? 404 : 200; + } + + if (done) { + done(status, text); + } + }; + addEventListenerFn(script, "load", callback); + addEventListenerFn(script, "error", callback); rawDocument.body.appendChild(script); - return doneWrapper; + return callback; } } @@ -18242,7 +19070,6 @@ var $interpolateMinErr = minErr('$interpolate'); /** * @ngdoc provider * @name $interpolateProvider - * @function * * @description * @@ -18260,7 +19087,7 @@ var $interpolateMinErr = minErr('$interpolate'); }); - customInterpolationApp.controller('DemoController', function DemoController() { + customInterpolationApp.controller('DemoController', function() { this.label = "This binding is brought you by // interpolation symbols."; }); </script> @@ -18288,7 +19115,7 @@ function $InterpolateProvider() { * @param {string=} value new value to set the starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value) { if (value) { startSymbol = value; return this; @@ -18306,7 +19133,7 @@ function $InterpolateProvider() { * @param {string=} value new value to set the ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value) { if (value) { endSymbol = value; return this; @@ -18318,12 +19145,18 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } /** * @ngdoc service * @name $interpolate - * @function + * @kind function * * @requires $parse * @requires $sce @@ -18342,6 +19175,62 @@ function $InterpolateProvider() { * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); * ``` * + * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is + * `true`, the interpolation function will return `undefined` unless all embedded expressions + * evaluate to a value other than `undefined`. + * + * ```js + * var $interpolate = ...; // injected + * var context = {greeting: 'Hello', name: undefined }; + * + * // default "forgiving" mode + * var exp = $interpolate('{{greeting}} {{name}}!'); + * expect(exp(context)).toEqual('Hello !'); + * + * // "allOrNothing" mode + * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); + * expect(exp(context)).toBeUndefined(); + * context.name = 'Angular'; + * expect(exp(context)).toEqual('Hello Angular!'); + * ``` + * + * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. + * + * ####Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * <example> + * <file name="index.html"> + * <div ng-init="username='A user'"> + * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} + * </p> + * <p><strong>{{username}}</strong> attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.</p> + * <p>Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.</p> + * </div> + * </file> + * </example> * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have @@ -18351,89 +19240,139 @@ function $InterpolateProvider() { * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. + * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined + * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ - function $interpolate(text, mustHaveExpression, trustedContext) { + function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + allOrNothing = !!allOrNothing; var startIndex, endIndex, index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, + expressions = [], + parseFns = [], + textLength = text.length, exp, - concat = []; - - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; + concat = [], + expressionPositions = []; + + while (index < textLength) { + if (((startIndex = text.indexOf(startSymbol, index)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { + if (index !== startIndex) { + concat.push(unescapeText(text.substring(index, startIndex))); + } + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; - hasInterpolation = true; + expressionPositions.push(concat.length); + concat.push(''); } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + concat.push(unescapeText(text.substring(index))); + } + break; } } - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; - } - // Concatenating expressions makes it hard to reason about whether some combination of // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a // single expression be used for iframe[src], object[src], etc., we ensure that the value // that's used is assigned or constructed by some JS code somewhere that is more testable or // 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 && parts.length > 1) { + 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); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i<ii; i++) { - if (typeof (part = parts[i]) == 'function') { - part = part(context); - if (trustedContext) { - part = $sce.getTrusted(trustedContext, part); - } else { - part = $sce.valueOf(part); - } - if (part === null || isUndefined(part)) { - part = ''; - } else if (typeof part != 'string') { - part = toJson(part); - } - } - concat[i] = part; - } - return concat.join(''); + if (!mustHaveExpression || expressions.length) { + var compute = function(values) { + for (var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; + concat[expressionPositions[i]] = values[i]; } - catch(err) { - var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, - err.toString()); - $exceptionHandler(newErr); + return concat.join(''); + }; + + var getValue = function(value) { + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $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; }; - fn.exp = text; - fn.parts = parts; - return fn; + + return extend(function interpolationFn(context) { + var i = 0; + var ii = expressions.length; + var values = new Array(ii); + + try { + for (; i < ii; i++) { + values[i] = parseFns[i](context); + } + + return compute(values); + } catch (err) { + var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, + err.toString()); + $exceptionHandler(newErr); + } + + }, { + // 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) { + var lastValue; + return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + var currValue = compute(values); + if (isFunction(listener)) { + 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); + } } } @@ -18444,7 +19383,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. * - * Use {@link ng.$interpolateProvider#startSymbol $interpolateProvider#startSymbol} to change + * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change * the symbol. * * @returns {string} start symbol. @@ -18460,7 +19399,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * - * Use {@link ng.$interpolateProvider#endSymbol $interpolateProvider#endSymbol} to change + * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change * the symbol. * * @returns {string} end symbol. @@ -18474,8 +19413,8 @@ function $InterpolateProvider() { } function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', - function($rootScope, $window, $q) { + this.$get = ['$rootScope', '$window', '$q', '$$q', + function($rootScope, $window, $q, $$q) { var intervals = {}; @@ -18514,55 +19453,56 @@ function $IntervalProvider() { * @returns {promise} A promise which will be notified on each iteration. * * @example - * <example module="time"> - * <file name="index.html"> - * <script> - * function Ctrl2($scope,$interval) { - * $scope.format = 'M/d/yy h:mm:ss a'; - * $scope.blood_1 = 100; - * $scope.blood_2 = 120; + * <example module="intervalExample"> + * <file name="index.html"> + * <script> + * angular.module('intervalExample', []) + * .controller('ExampleController', ['$scope', '$interval', + * function($scope, $interval) { + * $scope.format = 'M/d/yy h:mm:ss a'; + * $scope.blood_1 = 100; + * $scope.blood_2 = 120; * - * var stop; - * $scope.fight = function() { - * // Don't start a new fight if we are already fighting - * if ( angular.isDefined(stop) ) return; + * var stop; + * $scope.fight = function() { + * // Don't start a new fight if we are already fighting + * if ( angular.isDefined(stop) ) return; * - * stop = $interval(function() { - * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { + * stop = $interval(function() { + * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { * $scope.blood_1 = $scope.blood_1 - 3; * $scope.blood_2 = $scope.blood_2 - 4; - * } else { + * } else { * $scope.stopFight(); - * } - * }, 100); - * }; - * - * $scope.stopFight = function() { - * if (angular.isDefined(stop)) { - * $interval.cancel(stop); - * stop = undefined; - * } - * }; + * } + * }, 100); + * }; * - * $scope.resetFight = function() { - * $scope.blood_1 = 100; - * $scope.blood_2 = 120; - * } + * $scope.stopFight = function() { + * if (angular.isDefined(stop)) { + * $interval.cancel(stop); + * stop = undefined; + * } + * }; * - * $scope.$on('$destroy', function() { - * // Make sure that the interval is destroyed too - * $scope.stopFight(); - * }); - * } + * $scope.resetFight = function() { + * $scope.blood_1 = 100; + * $scope.blood_2 = 120; + * }; * - * angular.module('time', []) - * // Register the 'myCurrentTime' directive factory method. - * // We inject $interval and dateFilter service since the factory method is DI. - * .directive('myCurrentTime', function($interval, dateFilter) { + * $scope.$on('$destroy', function() { + * // Make sure that the interval is destroyed too + * $scope.stopFight(); + * }); + * }]) + * // Register the 'myCurrentTime' directive factory method. + * // We inject $interval and dateFilter service since the factory method is DI. + * .directive('myCurrentTime', ['$interval', 'dateFilter', + * function($interval, dateFilter) { * // return the directive link function. (compile function not needed) * return function(scope, element, attrs) { * var format, // date format - * stopTime; // so that we can cancel the time updates + * stopTime; // so that we can cancel the time updates * * // used to update the UI * function updateTime() { @@ -18578,37 +19518,37 @@ function $IntervalProvider() { * stopTime = $interval(updateTime, 1000); * * // listen on DOM destroy (removal) event, and cancel the next UI update - * // to prevent updating time ofter the DOM element was removed. - * element.bind('$destroy', function() { + * // to prevent updating time after the DOM element was removed. + * element.on('$destroy', function() { * $interval.cancel(stopTime); * }); * } - * }); - * </script> + * }]); + * </script> * - * <div> - * <div ng-controller="Ctrl2"> - * Date format: <input ng-model="format"> <hr/> - * Current time is: <span my-current-time="format"></span> - * <hr/> - * Blood 1 : <font color='red'>{{blood_1}}</font> - * Blood 2 : <font color='red'>{{blood_2}}</font> - * <button type="button" data-ng-click="fight()">Fight</button> - * <button type="button" data-ng-click="stopFight()">StopFight</button> - * <button type="button" data-ng-click="resetFight()">resetFight</button> - * </div> + * <div> + * <div ng-controller="ExampleController"> + * Date format: <input ng-model="format"> <hr/> + * Current time is: <span my-current-time="format"></span> + * <hr/> + * Blood 1 : <font color='red'>{{blood_1}}</font> + * Blood 2 : <font color='red'>{{blood_2}}</font> + * <button type="button" data-ng-click="fight()">Fight</button> + * <button type="button" data-ng-click="stopFight()">StopFight</button> + * <button type="button" data-ng-click="resetFight()">resetFight</button> * </div> + * </div> * - * </file> + * </file> * </example> */ function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = isDefined(count) ? count : 0; @@ -18646,7 +19586,7 @@ function $IntervalProvider() { interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); + $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } @@ -18667,7 +19607,7 @@ function $IntervalProvider() { * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -function $LocaleProvider(){ +function $LocaleProvider() { this.$get = function() { return { id: 'en-us', @@ -18710,7 +19650,7 @@ function $LocaleProvider(){ 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', + 'short': 'M/d/yy h:mm a', fullDate: 'EEEE, MMMM d, y', longDate: 'MMMM d, y', mediumDate: 'MMM d, y', @@ -18751,8 +19691,8 @@ function encodePath(path) { return segments.join('/'); } -function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { - var parsedUrl = urlResolve(absoluteUrl, appBase); +function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; @@ -18760,12 +19700,12 @@ function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { } -function parseAppUrl(relativeUrl, locationObj, appBase) { +function parseAppUrl(relativeUrl, locationObj) { var prefixed = (relativeUrl.charAt(0) !== '/'); if (prefixed) { relativeUrl = '/' + relativeUrl; } - var match = urlResolve(relativeUrl, appBase); + var match = urlResolve(relativeUrl); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); locationObj.$$search = parseKeyValue(match.search); @@ -18797,6 +19737,10 @@ function stripHash(url) { return index == -1 ? url : url.substr(0, index); } +function trimEmptyHash(url) { + return url.replace(/(#.+)|#$/, '$1'); +} + function stripFile(url) { return url.substr(0, stripHash(url).lastIndexOf('/') + 1); @@ -18820,12 +19764,12 @@ function LocationHtml5Url(appBase, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url + * @param {string} url HTML5 url * @private */ this.$$parse = function(url) { @@ -18835,7 +19779,7 @@ function LocationHtml5Url(appBase, basePrefix) { appBaseNoFile); } - parseAppUrl(pathUrl, this, appBase); + parseAppUrl(pathUrl, this); if (!this.$$path) { this.$$path = '/'; @@ -18856,21 +19800,32 @@ function LocationHtml5Url(appBase, basePrefix) { this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } var appUrl, prevAppUrl; + var rewrittenUrl; - if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + if ((appUrl = beginsWith(appBase, url)) !== undefined) { prevAppUrl = appUrl; - if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { + rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { - return appBase + prevAppUrl; + rewrittenUrl = appBase + prevAppUrl; } - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; + } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { + rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); } + return !!rewrittenUrl; }; } @@ -18887,7 +19842,7 @@ function LocationHtml5Url(appBase, basePrefix) { function LocationHashbangUrl(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** @@ -18897,17 +19852,26 @@ function LocationHashbangUrl(appBase, hashPrefix) { */ this.$$parse = function(url) { var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' - ? beginsWith(hashPrefix, withoutBaseUrl) - : (this.$$html5) - ? withoutBaseUrl - : ''; + var withoutHashUrl; - if (!isString(withoutHashUrl)) { - throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, - hashPrefix); + if (withoutBaseUrl.charAt(0) === '#') { + + // The rest of the url starts with a hash so we have + // got either a hashbang path or a plain hash fragment + withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); + if (isUndefined(withoutHashUrl)) { + // There was no hashbang prefix so we just have a hash fragment + withoutHashUrl = withoutBaseUrl; + } + + } else { + // There was no hashbang path nor hash fragment: + // If we are in HTML5 mode we use what is left as the path; + // Otherwise we ignore what is left + withoutHashUrl = this.$$html5 ? withoutBaseUrl : ''; } - parseAppUrl(withoutHashUrl, this, appBase); + + parseAppUrl(withoutHashUrl, this); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); @@ -18924,12 +19888,12 @@ function LocationHashbangUrl(appBase, hashPrefix) { * Inside of Angular, we're always using pathnames that * do not include drive names for routing. */ - function removeWindowsDriveName (path, url, base) { + function removeWindowsDriveName(path, url, base) { /* Matches paths for file protocol on windows, such as /C:/foo/bar, and captures only /foo/bar. */ - var windowsFilePathExp = /^\/?.*?:(\/.*)/; + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; var firstPathSegmentMatch; @@ -18938,10 +19902,7 @@ function LocationHashbangUrl(appBase, hashPrefix) { url = url.replace(base, ''); } - /* - * The input URL intentionally contains a - * first path segment that ends with a colon. - */ + // The input URL intentionally contains a first path segment that ends with a colon. if (windowsFilePathExp.exec(url)) { return path; } @@ -18963,10 +19924,12 @@ function LocationHashbangUrl(appBase, hashPrefix) { this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$rewrite = function(url) { - if(stripHash(appBase) == stripHash(url)) { - return url; + this.$$parseLinkUrl = function(url, relHref) { + if (stripHash(appBase) == stripHash(url)) { + this.$$parse(url); + return true; } + return false; }; } @@ -18986,23 +19949,43 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } + + var rewrittenUrl; var appUrl; - if ( appBase == stripHash(url) ) { - return url; - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; - } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; + if (appBase == stripHash(url)) { + rewrittenUrl = url; + } else if ((appUrl = beginsWith(appBaseNoFile, url))) { + rewrittenUrl = appBase + hashPrefix + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); } + return !!rewrittenUrl; + }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; }; + } -LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { +var locationPrototype = { /** * Are we in html5 mode? @@ -19011,7 +19994,7 @@ LocationHashbangInHtml5Url.prototype = $$html5: false, /** - * Has any change been replacing ? + * Has any change been replacing? * @private */ $$replace: false, @@ -19026,6 +20009,13 @@ LocationHashbangInHtml5Url.prototype = * Return full url representation with all segments encoded according to rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var absUrl = $location.absUrl(); + * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" + * ``` + * * @return {string} full url */ absUrl: locationGetter('$$absUrl'), @@ -19041,18 +20031,24 @@ LocationHashbangInHtml5Url.prototype = * * Change path, search and hash, when called with parameter and return `$location`. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var url = $location.url(); + * // => "/some/path?foo=bar&baz=xoxo" + * ``` + * * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed * @return {string} url */ - url: function(url, replace) { + url: function(url) { if (isUndefined(url)) return this.$$url; var match = PATH_MATCH.exec(url); - if (match[1]) this.path(decodeURIComponent(match[1])); - if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + if (match[1] || url === '') this.path(decodeURIComponent(match[1])); + if (match[2] || match[1] || url === '') this.search(match[3] || ''); + this.hash(match[5] || ''); return this; }, @@ -19066,6 +20062,13 @@ LocationHashbangInHtml5Url.prototype = * * Return protocol of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var protocol = $location.protocol(); + * // => "http" + * ``` + * * @return {string} protocol of current url */ protocol: locationGetter('$$protocol'), @@ -19079,6 +20082,13 @@ LocationHashbangInHtml5Url.prototype = * * Return host of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var host = $location.host(); + * // => "example.com" + * ``` + * * @return {string} host of current url. */ host: locationGetter('$$host'), @@ -19092,6 +20102,13 @@ LocationHashbangInHtml5Url.prototype = * * Return port of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var port = $location.port(); + * // => 80 + * ``` + * * @return {Number} port */ port: locationGetter('$$port'), @@ -19110,10 +20127,18 @@ LocationHashbangInHtml5Url.prototype = * Note: Path should always begin with forward slash (/), this method will add the forward slash * if it is missing. * - * @param {string=} path New path + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var path = $location.path(); + * // => "/some/path" + * ``` + * + * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { + path = path !== null ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), @@ -19128,24 +20153,55 @@ LocationHashbangInHtml5Url.prototype = * * Change search part when called with parameter and return `$location`. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // $location.search() => {foo: 'yipee', baz: 'xoxo'} + * ``` + * * @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or - * hash object. Hash object may contain an array of values, which will be decoded as duplicates in - * the url. + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the url. * - * @param {(string|Array<string>)=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If `paramValue` is an array, it will set the parameter as a - * comma-separated value. If `paramValue` is `null`, the parameter will be deleted. + * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` + * will override only a single search property. * - * @return {string} search + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. + * + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. + * + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { switch (arguments.length) { case 0: return this.$$search; case 1: - if (isString(search)) { + if (isString(search) || isNumber(search)) { + search = search.toString(); this.$$search = parseKeyValue(search); } else if (isObject(search)) { + search = copy(search, {}); + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); + this.$$search = search; } else { throw $locationMinErr('isrcharg', @@ -19175,10 +20231,19 @@ LocationHashbangInHtml5Url.prototype = * * Change hash fragment when called with parameter and return `$location`. * - * @param {string=} hash New hash fragment + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * var hash = $location.hash(); + * // => "hashValue" + * ``` + * + * @param {(string|number)=} hash New hash fragment * @return {string} hash */ - hash: locationGetterSetter('$$hash', identity), + hash: locationGetterSetter('$$hash', function(hash) { + return hash !== null ? hash.toString() : ''; + }), /** * @ngdoc method @@ -19194,6 +20259,46 @@ LocationHashbangInHtml5Url.prototype = } }; +forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { + Location.prototype = Object.create(locationPrototype); + + /** + * @ngdoc method + * @name $location#state + * + * @description + * This method is getter / setter. + * + * Return the history state object when called without any parameter. + * + * Change the history state object when called with one parameter and return `$location`. + * The state object is later passed to `pushState` or `replaceState`. + * + * NOTE: This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * older browsers (like IE9 or Android < 4.0), don't use this method. + * + * @param {object=} state State object for pushState or replaceState + * @return {object} state + */ + Location.prototype.state = function(state) { + if (!arguments.length) + return this.$$state; + + if (Location !== LocationHtml5Url || !this.$$html5) { + throw $locationMinErr('nostate', 'History API state support is available only ' + + 'in HTML5 mode and only in browsers supporting HTML5 History API'); + } + // The user might modify `stateObject` after invoking `$location.state(stateObject)` + // but we're changing the $$state reference to $browser.state() during the $digest + // so the modification window is narrow. + this.$$state = isUndefined(state) ? null : state; + + return this; + }; +}); + + function locationGetter(property) { return function() { return this[property]; @@ -19246,12 +20351,16 @@ function locationGetterSetter(property, preprocess) { * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ -function $LocationProvider(){ +function $LocationProvider() { var hashPrefix = '', - html5Mode = false; + html5Mode = { + enabled: false, + requireBase: true, + rewriteLinks: true + }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#hashPrefix * @description * @param {string=} prefix Prefix for hash part (containing path and search) @@ -19267,15 +20376,42 @@ function $LocationProvider(){ }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#html5Mode * @description - * @param {boolean=} mode Use HTML5 strategy if available. - * @returns {*} current value if used as getter or itself (chaining) if used as setter + * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. + * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported + * properties: + * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to + * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not + * support `pushState`. + * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies + * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are + * true, and a base tag is not present, an error will be thrown when `$location` is injected. + * See the {@link guide/$location $location guide for more information} + * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, + * enables/disables url rewriting for relative links. + * + * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { - if (isDefined(mode)) { - html5Mode = mode; + if (isBoolean(mode)) { + html5Mode.enabled = mode; + return this; + } else if (isObject(mode)) { + + if (isBoolean(mode.enabled)) { + html5Mode.enabled = mode.enabled; + } + + if (isBoolean(mode.requireBase)) { + html5Mode.requireBase = mode.requireBase; + } + + if (isBoolean(mode.rewriteLinks)) { + html5Mode.rewriteLinks = mode.rewriteLinks; + } + return this; } else { return html5Mode; @@ -19287,14 +20423,21 @@ function $LocationProvider(){ * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description - * Broadcasted before a URL will change. This change can be prevented by calling + * Broadcasted before a URL will change. + * + * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change - * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. + * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ /** @@ -19304,20 +20447,29 @@ function $LocationProvider(){ * @description * Broadcasted after a URL was changed. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', - function( $rootScope, $browser, $sniffer, $rootElement) { + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', + function($rootScope, $browser, $sniffer, $rootElement, $window) { var $location, LocationMode, baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' initialUrl = $browser.url(), appBase; - if (html5Mode) { + if (html5Mode.enabled) { + if (!baseHref && html5Mode.requireBase) { + throw $locationMinErr('nobase', + "$location in HTML5 mode requires a <base> tag to be present!"); + } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { @@ -19325,23 +20477,49 @@ function $LocationProvider(){ LocationMode = LocationHashbangUrl; } $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + $location.$$state = $browser.state(); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + + function setBrowserUrlWithFallback(url, replace, state) { + var oldUrl = $location.url(); + var oldState = $location.$$state; + try { + $browser.url(url, replace, state); + + // Make sure $location.state() returns referentially identical (not just deeply equal) + // state object; this makes possible quick checking if the state changed in the digest + // loop. Checking deep equality would be too expensive. + $location.$$state = $browser.state(); + } catch (e) { + // Restore old values if pushState fails + $location.url(oldUrl); + $location.$$state = oldState; + + throw e; + } + } $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during @@ -19349,16 +20527,21 @@ function $LocationProvider(){ absHref = urlResolve(absHref.animVal).href; } - var rewrittenUrl = $location.$$rewrite(absHref); + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. + event.preventDefault(); // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if ($location.absUrl() != $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + $window.angular['ff-684208-preventDefault'] = true; + } } } }); @@ -19369,52 +20552,82 @@ function $LocationProvider(){ $browser.url($location.absUrl(), true); } - // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); + var initializing = true; - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); - } + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl, newState) { + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + var oldState = $location.$$state; + var defaultPrevented; + + $location.$$parse(newUrl); + $location.$$state = newState; + + defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + setBrowserUrlWithFallback(oldUrl, false, oldState); + } else { + initializing = false; + afterLocationChange(oldUrl, oldState); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser - var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); + var oldUrl = trimEmptyHash($browser.url()); + var newUrl = trimEmptyHash($location.absUrl()); + var oldState = $browser.state(); var currentReplace = $location.$$replace; + var urlOrStateChanged = oldUrl !== newUrl || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + + if (initializing || urlOrStateChanged) { + initializing = false; - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { $location.$$parse(oldUrl); + $location.$$state = oldState; } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); } }); } + $location.$$replace = false; - return changeCounter; + // we don't need to return anything because $evalAsync will make the digest loop dirty when + // there is a change }); return $location; - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + function afterLocationChange(oldUrl, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); } }]; } @@ -19434,15 +20647,16 @@ function $LocationProvider(){ * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - <example> + <example module="logExample"> <file name="script.js"> - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); </file> <file name="index.html"> - <div ng-controller="LogCtrl"> + <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"/> @@ -19461,12 +20675,12 @@ function $LocationProvider(){ * @description * Use the `$logProvider` to configure how the application logs messages */ -function $LogProvider(){ +function $LogProvider() { var debug = true, self = this; /** - * @ngdoc property + * @ngdoc method * @name $logProvider#debugEnabled * @description * @param {boolean=} flag enable or disable debug level messages @@ -19481,7 +20695,7 @@ function $LogProvider(){ } }; - this.$get = ['$window', function($window){ + this.$get = ['$window', function($window) { return { /** * @ngdoc method @@ -19526,7 +20740,7 @@ function $LogProvider(){ * @description * Write a debug message */ - debug: (function () { + debug: (function() { var fn = consoleLog('debug'); return function() { @@ -19581,43 +20795,38 @@ function $LogProvider(){ } var $parseMinErr = minErr('$parse'); -var promiseWarningCache = {}; -var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ // Angular expressions are generally considered safe because these expressions only have direct -// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by +// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by // obtaining a reference to native JS functions such as the Function constructor. // // As an example, consider the following Angular expression: // -// {}.toString.constructor(alert("evil JS code")) -// -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". -// -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. +// {}.toString.constructor('alert("evil JS code")') // // This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits // against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good +// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good // practice and therefore we are not even trying to protect against interaction with an object // explicitly exposed in this way. // -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. -// // In general, it is not possible to access a Window object from an angular expression unless a // window or some DOM object that has a reference to window is published onto a Scope. +// Similarly we prevent invocations of function known to be dangerous, as well as assignments to +// native objects. +// +// See https://docs.angularjs.org/guide/security + function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { + if (name === "__defineGetter__" || name === "__defineSetter__" + || name === "__lookupGetter__" || name === "__lookupSetter__" + || name === "__proto__") { throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); + 'Attempting to access a disallowed field in Angular expressions! ' + + 'Expression: {0}', fullExpression); } return name; } @@ -19630,7 +20839,7 @@ function ensureSafeObject(obj, fullExpression) { 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { + obj.window === obj) { throw $parseMinErr('isecwindow', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); @@ -19639,18 +20848,54 @@ function ensureSafeObject(obj, fullExpression) { throw $parseMinErr('isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); + } else if (// block Object so that we can't get hold of dangerous Object.* methods + obj === Object) { + throw $parseMinErr('isecobj', + 'Referencing Object in Angular expressions is disallowed! Expression: {0}', + fullExpression); } } return obj; } -var OPERATORS = { - /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, - '+':function(self, locals, a,b){ +var CALL = Function.prototype.call; +var APPLY = Function.prototype.apply; +var BIND = Function.prototype.bind; + +function ensureSafeFunction(obj, fullExpression) { + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (obj === CALL || obj === APPLY || obj === BIND) { + throw $parseMinErr('isecff', + 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', + 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; + + +//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)) { @@ -19658,32 +20903,30 @@ var OPERATORS = { } return a; } - return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){ + 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); + 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);}, - '=':noop, - '===':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|b;}, - '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, - '!':function(self, locals, a){return !a(self, locals);} -}; -/* jshint bitwise: true */ + '*':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 ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -19693,85 +20936,51 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function (options) { +var Lexer = function(options) { this.options = options; }; Lexer.prototype = { constructor: Lexer, - lex: function (text) { + lex: function(text) { this.text = text; - this.index = 0; - this.ch = undefined; - this.lastCh = ':'; // can start regexp - this.tokens = []; - var token; - var json = []; - while (this.index < this.text.length) { - this.ch = this.text.charAt(this.index); - if (this.is('"\'')) { - this.readString(this.ch); - } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + var ch = this.text.charAt(this.index); + if (ch === '"' || ch === "'") { + this.readString(ch); + } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(this.ch)) { + } else if (this.isIdent(ch)) { this.readIdent(); - // identifiers can only be if the preceding char was a { or , - if (this.was('{,') && json[0] === '{' && - (token = this.tokens[this.tokens.length - 1])) { - token.json = token.text.indexOf('.') === -1; - } - } else if (this.is('(){}[].,;:?')) { - this.tokens.push({ - index: this.index, - text: this.ch, - json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') - }); - if (this.is('{[')) json.unshift(this.ch); - if (this.is('}]')) json.shift(); + } else if (this.is(ch, '(){}[].,;:?')) { + this.tokens.push({index: this.index, text: ch}); this.index++; - } else if (this.isWhitespace(this.ch)) { + } else if (this.isWhitespace(ch)) { this.index++; - continue; } else { - var ch2 = this.ch + this.peek(); + var ch2 = ch + this.peek(); var ch3 = ch2 + this.peek(2); - var fn = OPERATORS[this.ch]; - var fn2 = OPERATORS[ch2]; - var fn3 = OPERATORS[ch3]; - if (fn3) { - this.tokens.push({index: this.index, text: ch3, fn: fn3}); - this.index += 3; - } else if (fn2) { - this.tokens.push({index: this.index, text: ch2, fn: fn2}); - this.index += 2; - } else if (fn) { - this.tokens.push({ - index: this.index, - text: this.ch, - fn: fn, - json: (this.was('[,:') && this.is('+-')) - }); - this.index += 1; + var op1 = OPERATORS[ch]; + var op2 = OPERATORS[ch2]; + var op3 = OPERATORS[ch3]; + if (op1 || op2 || op3) { + var token = op3 ? ch3 : (op2 ? ch2 : ch); + this.tokens.push({index: this.index, text: token, operator: true}); + this.index += token.length; } else { this.throwError('Unexpected next character ', this.index, this.index + 1); } } - this.lastCh = this.ch; } return this.tokens; }, - is: function(chars) { - return chars.indexOf(this.ch) !== -1; - }, - - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; + is: function(ch, chars) { + return chars.indexOf(ch) !== -1; }, peek: function(i) { @@ -19780,7 +20989,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9'); + return ('0' <= ch && ch <= '9') && typeof ch === "string"; }, isWhitespace: function(ch) { @@ -19833,88 +21042,28 @@ Lexer.prototype = { } this.index++; } - number = 1 * number; this.tokens.push({ index: start, text: number, - json: true, - fn: function() { return number; } + constant: true, + value: Number(number) }); }, readIdent: function() { - var parser = this; - - var ident = ''; var start = this.index; - - var lastDot, peekIndex, methodName, ch; - while (this.index < this.text.length) { - ch = this.text.charAt(this.index); - if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { - if (ch === '.') lastDot = this.index; - ident += ch; - } else { + var ch = this.text.charAt(this.index); + if (!(this.isIdent(ch) || this.isNumber(ch))) { break; } this.index++; } - - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = this.index; - while (peekIndex < this.text.length) { - ch = this.text.charAt(peekIndex); - if (ch === '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - this.index = peekIndex; - break; - } - if (this.isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } - } - - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.json = OPERATORS[ident]; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); - - if (methodName) { - this.tokens.push({ - index:lastDot, - text: '.', - json: false - }); - this.tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } + text: this.text.slice(start, this.index), + identifier: true + }); }, readString: function(quote) { @@ -19935,11 +21084,7 @@ Lexer.prototype = { string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { @@ -19949,9 +21094,8 @@ Lexer.prototype = { this.tokens.push({ index: start, text: rawString, - string: string, - json: true, - fn: function() { return string; } + constant: true, + value: string }); return; } else { @@ -19964,46 +21108,34 @@ Lexer.prototype = { }; +function isConstant(exp) { + return exp.constant; +} + /** * @constructor */ -var Parser = function (lexer, $filter, options) { +var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; }; -Parser.ZERO = extend(function () { +Parser.ZERO = extend(function() { return 0; }, { + sharedGetter: true, constant: true }); Parser.prototype = { constructor: Parser, - parse: function (text, json) { + parse: function(text) { this.text = text; - - //TODO(i): strip all the obsolte json stuff from this file - this.json = json; - this.tokens = this.lexer.lex(text); - if (json) { - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - this.assignment = this.logicalOR; - - this.functionCall = - this.fieldAccess = - this.objectIndex = - this.filterChain = function() { - this.throwError('is not valid json', {text: text, index: 0}); - }; - } - - var value = json ? this.primary() : this.statements(); + var value = this.statements(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); @@ -20015,7 +21147,7 @@ Parser.prototype = { return value; }, - primary: function () { + primary: function() { var primary; if (this.expect('(')) { primary = this.filterChain(); @@ -20024,16 +21156,14 @@ Parser.prototype = { 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 { - var token = this.expect(); - primary = token.fn; - if (!primary) { - this.throwError('not a primary expression', token); - } - if (token.json) { - primary.constant = true; - primary.literal = true; - } + this.throwError('not a primary expression', this.peek()); } var next, context; @@ -20067,8 +21197,11 @@ Parser.prototype = { }, peek: function(e1, e2, e3, e4) { - if (this.tokens.length > 0) { - var token = this.tokens[0]; + 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)) { @@ -20078,45 +21211,66 @@ Parser.prototype = { return false; }, - expect: function(e1, e2, e3, e4){ + expect: function(e1, e2, e3, e4) { var token = this.peek(e1, e2, e3, e4); if (token) { - if (this.json && !token.json) { - this.throwError('is not valid json', token); - } this.tokens.shift(); return token; } return false; }, - consume: function(e1){ - if (!this.expect(e1)) { + 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(fn, right) { - return extend(function(self, locals) { + unaryFn: function(op, right) { + var fn = OPERATORS[op]; + return extend(function $parseUnaryFn(self, locals) { return fn(self, locals, right); }, { - constant:right.constant + constant:right.constant, + inputs: [right] }); }, - ternaryFn: function(left, middle, right){ - return extend(function(self, locals){ - return left(self, locals) ? middle(self, locals) : right(self, locals); + 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 && middle.constant && right.constant + constant: left.constant && right.constant, + inputs: !isBranching && [left, right] }); }, - binaryFn: function(left, fn, right) { - return extend(function(self, locals) { - return fn(self, locals, 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:left.constant && right.constant + constant: true, + literal: true }); }, @@ -20130,13 +21284,10 @@ Parser.prototype = { // TODO(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] - : function(self, locals) { + : function $parseStatements(self, locals) { var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } + for (var i = 0, ii = statements.length; i < ii; i++) { + value = statements[i](self, locals); } return value; }; @@ -20147,35 +21298,45 @@ Parser.prototype = { filterChain: function() { var left = this.expression(); var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } + while ((token = this.expect('|'))) { + left = this.filter(left); } + return left; }, - filter: function() { - var token = this.expect(); - var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { + 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()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; } } + + 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() { @@ -20192,9 +21353,11 @@ Parser.prototype = { this.text.substring(0, token.index) + '] can not be assigned to', token); } right = this.ternary(); - return function(scope, locals) { + return extend(function $parseAssignment(scope, locals) { return left.assign(scope, right(scope, locals), locals); - }; + }, { + inputs: [left, right] + }); } return left; }, @@ -20204,34 +21367,35 @@ Parser.prototype = { var middle; var token; if ((token = this.expect('?'))) { - middle = this.ternary(); - if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); - } else { - this.throwError('expected :', token); + middle = this.assignment(); + 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 + }); } - } else { - return left; } + + return left; }, logicalOR: function() { var left = this.logicalAND(); var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while ((token = this.expect('||'))) { + left = this.binaryFn(left, token.text, this.logicalAND(), true); } + return left; }, logicalAND: function() { var left = this.equality(); var token; - if ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); + while ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.text, this.equality(), true); } return left; }, @@ -20239,8 +21403,8 @@ Parser.prototype = { equality: function() { var left = this.relational(); var token; - if ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.fn, this.equality()); + while ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.text, this.relational()); } return left; }, @@ -20248,8 +21412,8 @@ Parser.prototype = { relational: function() { var left = this.additive(); var token; - if ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.fn, this.relational()); + while ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.text, this.additive()); } return left; }, @@ -20258,7 +21422,7 @@ Parser.prototype = { var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.fn, this.multiplicative()); + left = this.binaryFn(left, token.text, this.multiplicative()); } return left; }, @@ -20267,7 +21431,7 @@ Parser.prototype = { var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.fn, this.unary()); + left = this.binaryFn(left, token.text, this.unary()); } return left; }, @@ -20277,61 +21441,56 @@ Parser.prototype = { if (this.expect('+')) { return this.primary(); } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + return this.binaryFn(Parser.ZERO, token.text, this.unary()); } else if ((token = this.expect('!'))) { - return this.unaryFn(token.fn, this.unary()); + return this.unaryFn(token.text, this.unary()); } else { return this.primary(); } }, fieldAccess: function(object) { - var parser = this; - var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + var getter = this.identifier(); - return extend(function(scope, locals, self) { - return getter(self || object(scope, locals)); + return extend(function $parseFieldAccess(scope, locals, self) { + var o = self || object(scope, locals); + return (o == null) ? undefined : getter(o); }, { assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); + var o = object(scope, locals); + if (!o) object.assign(scope, o = {}); + return getter.assign(o, value); } }); }, objectIndex: function(obj) { - var parser = this; + var expression = this.text; var indexFn = this.expression(); this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseObjectIndex(self, locals) { var o = obj(self, locals), i = indexFn(self, locals), - v, p; + v; + ensureSafeMemberName(i, expression); if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } + v = ensureSafeObject(o[i], expression); return v; }, { assign: function(self, value, locals) { - var key = indexFn(self, locals); + var key = ensureSafeMemberName(indexFn(self, locals), expression); // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; + var o = ensureSafeObject(obj(self, locals), expression); + if (!o) obj.assign(self, o = {}); + return o[key] = value; } }); }, - functionCall: function(fn, contextGetter) { + functionCall: function(fnGetter, contextGetter) { var argsFn = []; if (this.peekToken().text !== ')') { do { @@ -20340,91 +21499,92 @@ Parser.prototype = { } this.consume(')'); - var parser = this; + var expressionText = this.text; + // we can safely reuse the array across invocations + var args = argsFn.length ? [] : null; - return function(scope, locals) { - var args = []; - var context = contextGetter ? contextGetter(scope, locals) : scope; + return function $parseFunctionCall(scope, locals) { + var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; + var fn = fnGetter(scope, locals, context) || noop; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); + if (args) { + var i = argsFn.length; + while (i--) { + args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); + } } - var fnPtr = fn(scope, locals, context) || noop; - ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); + ensureSafeObject(context, expressionText); + ensureSafeFunction(fn, expressionText); - // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + // 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 ensureSafeObject(v, parser.text); - }; + return ensureSafeObject(v, expressionText); + }; }, // This is used with json array declaration - arrayDeclaration: function () { + arrayDeclaration: function() { var elementFns = []; - var allConstant = true; if (this.peekToken().text !== ']') { do { if (this.peek(']')) { // Support trailing commas per ES5.1. break; } - var elementFn = this.expression(); - elementFns.push(elementFn); - if (!elementFn.constant) { - allConstant = false; - } + elementFns.push(this.expression()); } while (this.expect(',')); } this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseArrayLiteral(self, locals) { var array = []; - for (var i = 0; i < elementFns.length; i++) { + for (var i = 0, ii = elementFns.length; i < ii; i++) { array.push(elementFns[i](self, locals)); } return array; }, { literal: true, - constant: allConstant + constant: elementFns.every(isConstant), + inputs: elementFns }); }, - object: function () { - var keyValues = []; - var allConstant = true; + object: function() { + var keys = [], valueFns = []; if (this.peekToken().text !== '}') { do { if (this.peek('}')) { // Support trailing commas per ES5.1. break; } - var token = this.expect(), - key = token.string || token.text; - this.consume(':'); - var value = this.expression(); - keyValues.push({key: key, value: value}); - if (!value.constant) { - allConstant = false; + var token = this.consume(); + if (token.constant) { + keys.push(token.value); + } else if (token.identifier) { + keys.push(token.text); + } else { + this.throwError("invalid key", token); } + this.consume(':'); + valueFns.push(this.expression()); } while (this.expect(',')); } this.consume('}'); - return extend(function(self, locals) { + return extend(function $parseObjectLiteral(self, locals) { var object = {}; - for (var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); + for (var i = 0, ii = valueFns.length; i < ii; i++) { + object[keys[i]] = valueFns[i](self, locals); } return object; }, { literal: true, - constant: allConstant + constant: valueFns.every(isConstant), + inputs: valueFns }); } }; @@ -20434,196 +21594,104 @@ Parser.prototype = { // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; +function setter(obj, path, setValue, fullExp) { + ensureSafeObject(obj, fullExp); var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; + var propertyObj = ensureSafeObject(obj[key], fullExp); if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } } key = ensureSafeMemberName(element.shift(), fullExp); + ensureSafeObject(obj[key], fullExp); obj[key] = setValue; return setValue; } -var getterFnCache = {}; +var getterFnCacheDefault = createMap(); +var getterFnCacheExpensive = createMap(); + +function isPossiblyDangerousMemberName(name) { + return name == 'constructor'; +} /** * 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, options) { +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; - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; + return function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; + if (pathVal == null) return pathVal; + pathVal = eso0(pathVal[key0]); - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso1(pathVal[key1]); - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso2(pathVal[key2]); - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso3(pathVal[key3]); - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso4(pathVal[key4]); - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; -} - -function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); - - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + return pathVal; }; } -function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; +function getterFnWithEnsureSafeObject(fn, fullExpression) { + return function(s, l) { + return fn(s, l, ensureSafeObject, fullExpression); }; } function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } + var expensiveChecks = options.expensiveChecks; + var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); + var fn = getterFnCache[path]; + if (fn) return fn; + var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; + pathKeysLength = pathKeys.length; - // When we have only 1 or 2 tokens, use optimized special case closures. // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { + if (options.csp) { if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks); } else { - fn = function(scope, locals) { + fn = function cspSafeGetter(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); + pathKeys[i++], fullExp, expensiveChecks)(scope, locals); locals = undefined; // clear after first iteration scope = val; @@ -20632,46 +21700,51 @@ function getterFn(path, options, fullExp) { }; } } else { - var code = 'var p;\n'; + 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); - code += 'if(s == null) return undefined;\n' + - 's='+ (index + 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 - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); + : '((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'; }); code += 'return s;'; /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; + if (needsEnsureSafeObject) { + evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp); + } + fn = evaledFnGetter; } - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; - } + fn.sharedGetter = true; + fn.assign = function(self, value) { + return setter(self, path, value, path); + }; + getterFnCache[path] = fn; return fn; } +var objectValueOf = Object.prototype.valueOf; + +function getValueOf(value) { + return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); +} + /////////////////////////////////// /** @@ -20718,142 +21791,257 @@ function getterFn(path, options, fullExp) { /** * @ngdoc provider * @name $parseProvider - * @function * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cache = {}; + var cacheDefault = createMap(); + var cacheExpensive = createMap(); - var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#unwrapPromises - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } - }; + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + var $parseOptions = { + csp: $sniffer.csp, + expensiveChecks: false + }, + $parseOptionsExpensive = { + csp: $sniffer.csp, + expensiveChecks: true + }; + function wrapSharedExpression(exp) { + var wrapped = exp; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#logPromiseWarnings - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; + if (exp.sharedGetter) { + wrapped = function $parseWrapper(self, locals) { + return exp(self, locals); + }; + wrapped.literal = exp.literal; + wrapped.constant = exp.constant; + wrapped.assign = exp.assign; + } + + return wrapped; } - }; + return function $parse(exp, interceptorFn, expensiveChecks) { + var parsedExpression, oneTime, cacheKey; - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; + switch (typeof exp) { + case 'string': + cacheKey = exp = exp.trim(); + + var cache = (expensiveChecks ? cacheExpensive : cacheDefault); + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + 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; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; + } + + cache[cacheKey] = parsedExpression; + } + return addInterceptor(parsedExpression, interceptorFn); - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + case 'function': + return addInterceptor(exp, interceptorFn); + + default: + return addInterceptor(noop, interceptorFn); + } }; - return function(exp) { - var parsedExpression; + 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); + } + } + } - switch (typeof exp) { - case 'string': + return list; + } + + function expressionInputDirtyCheck(newValue, oldValueOfValue) { + + if (newValue == null || oldValueOfValue == null) { // null/undefined + return newValue === oldValueOfValue; + } - if (cache.hasOwnProperty(exp)) { - return cache[exp]; + if (typeof newValue === 'object') { + + // attempt to convert the value to a primitive type + // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can + // be cheaply dirty-checked + newValue = getValueOf(newValue); + + if (typeof newValue === 'object') { + // objects/arrays are not supported - deep-watching them would be too expensive + return false; + } + + // fall-through to the primitive equality check + } + + //Primitive or NaN + return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); + } + + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var inputExpressions = parsedExpression.$$inputs || + (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); + + var lastResult; + + if (inputExpressions.length === 1) { + var oldInputValue = 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); } + return lastResult; + }, listener, objectEquality); + } - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp, false); + var oldInputValueOfValues = []; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + } + + return scope.$watch(function expressionInputsWatch(scope) { + var changed = false; - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + var newInputValue = inputExpressions[i](scope); + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } + } - return parsedExpression; + if (changed) { + lastResult = parsedExpression(scope); + } - case 'function': - return exp; + return lastResult; + }, listener, objectEquality); + } - default: - return noop; + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.apply(this, arguments); + } + if (isDefined(value)) { + scope.$$postDigest(function() { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + }, objectEquality); + } + + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.call(this, value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function() { + if (isAllDefined(lastValue)) unwatch(); + }); + } + }, objectEquality); + + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; } - }; + } + + function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch; + return unwatch = scope.$watch(function constantWatch(scope) { + return parsedExpression(scope); + }, function constantListener(value, old, scope) { + if (isFunction(listener)) { + listener.apply(this, arguments); + } + unwatch(); + }, objectEquality); + } + + function addInterceptor(parsedExpression, interceptorFn) { + if (!interceptorFn) return parsedExpression; + var watchDelegate = parsedExpression.$$watchDelegate; + + var regularWatch = + watchDelegate !== oneTimeLiteralWatchDelegate && + watchDelegate !== oneTimeWatchDelegate; + + var fn = regularWatch ? function regularInterceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + return interceptorFn(value, scope, locals); + } : function oneTimeInterceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; + + // Propagate $$watchDelegates other then inputsWatchDelegate + if (parsedExpression.$$watchDelegate && + parsedExpression.$$watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + } else if (!interceptorFn.$stateful) { + // 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]; + } + + return fn; + } }]; } @@ -20863,7 +22051,54 @@ function $ParseProvider() { * @requires $rootScope * * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. + * + * This is an implementation of promises/deferred objects inspired by + * [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred + * implementations, and the other which resembles ES6 promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * available yet. + * + * It can be used like so: + * + * ```js + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * return $q(function(resolve, reject) { + * setTimeout(function() { + * if (okToGreet(name)) { + * resolve('Hello, ' + name + '!'); + * } else { + * reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * }); + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }); + * ``` + * + * Note: progress/notify callbacks are not currently supported via the ES6-style interface. + * + * 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 * interface for interacting with an object that represents the result of an action that is @@ -20873,24 +22108,20 @@ function $ParseProvider() { * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * * ```js - * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` * // are available in the current lexical scope (they could have been injected or passed in). * * function asyncGreet(name) { * var deferred = $q.defer(); * * setTimeout(function() { - * // since this fn executes async in a future turn of the event loop, we need to wrap - * // our code into an $apply call so that the model changes are properly observed. - * scope.$apply(function() { - * deferred.notify('About to greet ' + name + '.'); + * deferred.notify('About to greet ' + name + '.'); * - * if (okToGreet(name)) { - * deferred.resolve('Hello, ' + name + '!'); - * } else { - * deferred.reject('Greeting ' + name + ' is not allowed.'); - * } - * }); + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } * }, 1000); * * return deferred.promise; @@ -20915,7 +22146,6 @@ function $ParseProvider() { * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. @@ -20956,21 +22186,17 @@ function $ParseProvider() { * * 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 can not be resolved or rejected from the notifyCallback + * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback * method. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * - * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, * but to do so without modifying the final value. This is useful to release resources or do some * clean-up that needs to be done whether the promise was rejected or resolved. See the [full * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * - * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as - * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 and Android 2.x compatible. - * * # Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily @@ -21024,6 +22250,12 @@ function $ParseProvider() { * expect(resolvedValue).toEqual(123); * })); * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. */ function $QProvider() { @@ -21034,21 +22266,41 @@ function $QProvider() { }]; } +function $$QProvider() { + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler); + }]; +} /** * Constructs a promise manager. * - * @param {function(Function)} nextTick Function for executing functions in the next turn. + * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. * @returns {object} Promise manager. */ function qFactory(nextTick, exceptionHandler) { + var $qMinErr = minErr('$q', TypeError); + function callOnce(self, resolveFn, rejectFn) { + var called = false; + function wrap(fn) { + return function(value) { + if (called) return; + called = true; + fn.call(self, value); + }; + } + + return [wrap(resolveFn), wrap(rejectFn)]; + } /** * @ngdoc method - * @name $q#defer - * @function + * @name ng.$q#defer + * @kind function * * @description * Creates a `Deferred` object which represents a task which will finish in the future. @@ -21056,156 +22308,152 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Deferred} Returns a new instance of deferred. */ var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, - + return new Deferred(); + }; - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, + function Promise() { + this.$$state = { status: 0 }; + } + Promise.prototype = { + then: function(onFulfilled, onRejected, progressBack) { + var result = new Deferred(); - notify: function(progress) { - if (pending) { - var callbacks = pending; + this.$$state.pending = this.$$state.pending || []; + this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); + if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, + return result.promise; + }, + "catch": function(callback) { + return this.then(null, callback); + }, - promise: { - then: function(callback, errback, progressback) { - var result = defer(); + "finally": function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, true, callback); + }, function(error) { + return handleCallback(error, false, callback); + }, progressBack); + } + }; - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native + function simpleBind(context, fn) { + return function(value) { + fn.call(context, value); + }; + } - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + function processQueue(state) { + var fn, promise, pending; - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; - - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); - } + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + for (var i = 0, ii = pending.length; i < ii; ++i) { + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + promise.resolve(fn(state.value)); + } else if (state.status === 1) { + promise.resolve(state.value); + } else { + promise.reject(state.value); + } + } catch (e) { + promise.reject(e); + exceptionHandler(e); + } + } + } - return result.promise; - }, + function scheduleProcessQueue(state) { + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + nextTick(function() { processQueue(state); }); + } - "catch": function(callback) { - return this.then(null, callback); - }, + function Deferred() { + this.promise = new Promise(); + //Necessary to support unbound execution :/ + this.resolve = simpleBind(this, this.resolve); + this.reject = simpleBind(this, this.reject); + this.notify = simpleBind(this, this.notify); + } - "finally": function(callback) { + Deferred.prototype = { + resolve: function(val) { + if (this.promise.$$state.status) return; + if (val === this.promise) { + this.$$reject($qMinErr( + 'qcycle', + "Expected promise to be resolved with value other than itself '{0}'", + val)); + } + else { + this.$$resolve(val); + } - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } + }, - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } + $$resolve: function(val) { + var then, fns; - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); + fns = callOnce(this, this.$$resolve, this.$$reject); + try { + if ((isObject(val) || isFunction(val))) then = val && val.then; + if (isFunction(then)) { + this.promise.$$state.status = -1; + then.call(val, fns[0], fns[1], this.notify); + } else { + this.promise.$$state.value = val; + this.promise.$$state.status = 1; + scheduleProcessQueue(this.promise.$$state); } + } catch (e) { + fns[1](e); + exceptionHandler(e); } - }; + }, - return deferred; - }; + reject: function(reason) { + if (this.promise.$$state.status) return; + this.$$reject(reason); + }, + $$reject: function(reason) { + this.promise.$$state.value = reason; + this.promise.$$state.status = 2; + scheduleProcessQueue(this.promise.$$state); + }, - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); + notify: function(progress) { + var callbacks = this.promise.$$state.pending; + + if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { nextTick(function() { - result.resolve(callback(value)); + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + result.notify(isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } }); - return result.promise; } - }; + } }; - /** * @ngdoc method * @name $q#reject - * @function + * @kind function * * @description * Creates a promise that is resolved as rejected with the specified `reason`. This api should be @@ -21239,33 +22487,43 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ var reject = function(reason) { - var result = defer(); + var result = new Deferred(); result.reject(reason); return result.promise; }; - var createInternalRejectedPromise = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }); - return result.promise; - } - }; + var makePromise = function makePromise(value, resolved) { + var result = new Deferred(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; }; + var handleCallback = function handleCallback(value, isResolved, callback) { + var callbackOutput = null; + try { + if (isFunction(callback)) callbackOutput = callback(); + } catch (e) { + return makePromise(e, false); + } + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + }; /** * @ngdoc method * @name $q#when - * @function + * @kind function * * @description * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. @@ -21275,69 +22533,18 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} value Value or a promise * @returns {Promise} Returns a promise of the passed value or promise */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; + var when = function(value, callback, errback, progressBack) { + var result = new Deferred(); + result.resolve(value); + return result.promise.then(callback, errback, progressBack); }; - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - /** * @ngdoc method * @name $q#all - * @function + * @kind function * * @description * Combines multiple promises into a single promise that is resolved when all of the input @@ -21349,14 +22556,15 @@ function qFactory(nextTick, exceptionHandler) { * If any of the promises is resolved with a rejection, this resulting promise will be rejected * with the same rejection value. */ + function all(promises) { - var deferred = defer(), + var deferred = new Deferred(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; - ref(promise).then(function(value) { + when(promise).then(function(value) { if (results.hasOwnProperty(key)) return; results[key] = value; if (!(--counter)) deferred.resolve(results); @@ -21373,23 +22581,46 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all + var $Q = function Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); + } + + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + + var deferred = new Deferred(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.promise; }; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.all = all; + + return $Q; } -function $$RAFProvider(){ //rAF +function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || - $window.webkitRequestAnimationFrame || - $window.mozRequestAnimationFrame; + $window.webkitRequestAnimationFrame; var cancelAnimationFrame = $window.cancelAnimationFrame || $window.webkitCancelAnimationFrame || - $window.mozCancelAnimationFrame || $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; @@ -21428,7 +22659,7 @@ function $$RAFProvider(){ //rAF * * 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 - * items to the array at the beginning (shift) instead of at the end (push) + * 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 @@ -21480,10 +22711,11 @@ function $$RAFProvider(){ //rAF * They also provide an event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ -function $RootScopeProvider(){ +function $RootScopeProvider() { var TTL = 10; var $rootScopeMinErr = minErr('$rootScope'); var lastDirtyWatch = null; + var applyAsyncId = null; this.digestTtl = function(value) { if (arguments.length) { @@ -21493,7 +22725,7 @@ function $RootScopeProvider(){ }; this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function( $injector, $exceptionHandler, $parse, $browser) { + function($injector, $exceptionHandler, $parse, $browser) { /** * @ngdoc type @@ -21517,7 +22749,6 @@ function $RootScopeProvider(){ var child = parent.$new(); parent.salutation = "Hello"; - child.name = "World"; expect(child.salutation).toEqual('Hello'); child.salutation = "Welcome"; @@ -21525,6 +22756,10 @@ function $RootScopeProvider(){ expect(parent.salutation).toEqual('Hello'); * ``` * + * When interacting with `Scope` in tests, additional helper methods are available on the + * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional + * details. + * * * @param {Object.<string, function()>=} providers Map of service factory which need to be * provided for the current scope. Defaults to {@link ng}. @@ -21540,36 +22775,49 @@ function $RootScopeProvider(){ this.$$phase = this.$parent = this.$$watchers = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; + this.$root = this; this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; this.$$listeners = {}; this.$$listenerCount = {}; - this.$$isolateBindings = {}; + this.$$isolateBindings = null; } /** * @ngdoc property * @name $rootScope.Scope#$id - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. */ + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$root + * + * @description + * Reference to the root scope. + */ Scope.prototype = { constructor: Scope, /** * @ngdoc method * @name $rootScope.Scope#$new - * @function + * @kind function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and @@ -21580,46 +22828,66 @@ function $RootScopeProvider(){ * When creating widgets, it is useful for the widget to not accidentally read parent * state. * + * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` + * of the newly created scope. Defaults to `this` scope if not provided. + * This is used when creating a transclude scope to correctly place it + * in the scope hierarchy while maintaining the correct prototypical + * inheritance. + * * @returns {Object} The newly created child scope. * */ - $new: function(isolate) { - var ChildScope, - child; + $new: function(isolate, parent) { + var child; + + parent = parent || this; if (isolate) { child = new Scope(); child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; } else { - ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. This will then show up as class - // name in the web inspector. - ChildScope.prototype = this; - child = new ChildScope(); - child.$id = nextUid(); + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$ChildScope) { + this.$$ChildScope = function ChildScope() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$id = nextUid(); + this.$$ChildScope = null; + }; + this.$$ChildScope.prototype = this; + } + child = new this.$$ChildScope(); } - child['this'] = child; - child.$$listeners = {}; - child.$$listenerCount = {}; - child.$parent = this; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; + child.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; } else { - this.$$childHead = this.$$childTail = child; + parent.$$childHead = parent.$$childTail = child; } + + // When the new scope is not isolated or we inherit from `this`, and + // the parent scope is destroyed, the property `$$destroyed` is inherited + // prototypically. In all other cases, this property needs to be set + // when the parent scope is destroyed. + // The listener needs to be added after the parent is set + if (isolate || parent != this) child.$on('$destroy', destroyChild); + return child; + + function destroyChild() { + child.$$destroyed = true; + } }, /** * @ngdoc method * @name $rootScope.Scope#$watch - * @function + * @kind function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. @@ -21631,10 +22899,14 @@ function $RootScopeProvider(){ * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - 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). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, - * the {@link angular.copy} function is used. It also means that watching complex options - * will have adverse memory and performance implications. + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -21652,7 +22924,6 @@ function $RootScopeProvider(){ * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * - * The example below contains an illustration of using a function as your $watch listener * * * # Example @@ -21669,23 +22940,27 @@ function $RootScopeProvider(){ expect(scope.counter).toEqual(0); scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - scope.name = 'adam'; scope.$digest(); + // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); + - // Using a listener function + // Using a function as a watchExpression var food; scope.foodCounter = 0; expect(scope.foodCounter).toEqual(0); scope.$watch( - // This is the listener function + // This function returns the value being watched. It is called for each turn of the $digest loop function() { return food; }, - // This is the change handler + // This is the change listener, called when the value returned from the above function changes function(newValue, oldValue) { if ( newValue !== oldValue ) { // Only increment the counter if the value changed @@ -21715,20 +22990,23 @@ function $RootScopeProvider(){ * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. * + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { + var get = $parse(watchExp); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, get); + } var scope = this, - get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, @@ -21740,18 +23018,8 @@ function $RootScopeProvider(){ lastDirtyWatch = null; - // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; + watcher.fn = noop; } if (!array) { @@ -21761,17 +23029,100 @@ function $RootScopeProvider(){ // the while loop reads in reverse order. array.unshift(watcher); - return function() { + return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$watchGroup + * @kind function + * + * @description + * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. + * If any one expression in the collection changes the `listener` is executed. + * + * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every + * call to $digest() to see if any items changes. + * - The `listener` is called whenever any expression in the `watchExpressions` array changes. + * + * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * The `scope` refers to the current scope. + * @returns {function()} Returns a de-registration function for all listeners. + */ + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var self = this; + var changeReactionScheduled = false; + var firstRun = true; + + if (!watchExpressions.length) { + // No expressions means we call the listener ASAP + var shouldCall = true; + self.$evalAsync(function() { + if (shouldCall) listener(newValues, newValues, self); + }); + return function deregisterWatchGroup() { + shouldCall = false; + }; + } + + if (watchExpressions.length === 1) { + // Special case size of one + return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { + newValues[0] = value; + oldValues[0] = oldValue; + listener(newValues, (value === oldValue) ? newValues : oldValues, scope); + }); + } + + forEach(watchExpressions, function(expr, i) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + newValues[i] = value; + oldValues[i] = oldValue; + if (!changeReactionScheduled) { + changeReactionScheduled = true; + self.$evalAsync(watchGroupAction); + } + }); + deregisterFns.push(unwatchFn); + }); + + function watchGroupAction() { + changeReactionScheduled = false; + + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } + + return function deregisterWatchGroup() { + while (deregisterFns.length) { + deregisterFns.shift()(); + } + }; + }, + /** * @ngdoc method * @name $rootScope.Scope#$watchCollection - * @function + * @kind function * * @description * Shallow watches the properties of an object and fires whenever any of the properties change @@ -21824,6 +23175,8 @@ function $RootScopeProvider(){ * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { + $watchCollectionInterceptor.$stateful = true; + var self = this; // the current value, updated on each dirty-check run var newValue; @@ -21835,15 +23188,18 @@ function $RootScopeProvider(){ // only track veryOldValue if the listener is asking for it var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; - var objGetter = $parse(obj); + var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; + + // If the new value is undefined, then return undefined as the watch may be a one-time watch + if (isUndefined(newValue)) return; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { @@ -21867,11 +23223,13 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - var bothNaN = (oldValue[i] !== oldValue[i]) && - (newValue[i] !== newValue[i]); - if (!bothNaN && (oldValue[i] !== newValue[i])) { + oldItem = oldValue[i]; + newItem = newValue[i]; + + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[i] = newValue[i]; + oldValue[i] = newItem; } } } else { @@ -21886,14 +23244,18 @@ function $RootScopeProvider(){ for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { + newItem = newValue[key]; + oldItem = oldValue[key]; + + if (key in oldValue) { + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; } } else { oldLength++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; changeDetected++; } } @@ -21901,8 +23263,8 @@ function $RootScopeProvider(){ if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; - for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + for (key in oldValue) { + if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } @@ -21941,13 +23303,13 @@ function $RootScopeProvider(){ } } - return this.$watch($watchCollectionWatch, $watchCollectionAction); + return this.$watch(changeDetector, $watchCollectionAction); }, /** * @ngdoc method * @name $rootScope.Scope#$digest - * @function + * @kind function * * @description * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and @@ -21961,7 +23323,7 @@ function $RootScopeProvider(){ * {@link ng.directive:ngController controllers} or in * {@link ng.$compileProvider#directive directives}. * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within - * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. + * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with @@ -21982,20 +23344,22 @@ function $RootScopeProvider(){ expect(scope.counter).toEqual(0); scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - scope.name = 'adam'; scope.$digest(); + // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); * ``` * */ $digest: function() { var watch, value, last, watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, @@ -22003,6 +23367,15 @@ function $RootScopeProvider(){ logIdx, logMsg, asyncTask; beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); + + if (this === $rootScope && applyAsyncId !== null) { + // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then + // cancel the scheduled $apply and flush the queue of expressions to be evaluated. + $browser.defer.cancel(applyAsyncId); + flushApplyAsync(); + } lastDirtyWatch = null; @@ -22010,12 +23383,11 @@ function $RootScopeProvider(){ dirty = false; current = target; - while(asyncQueue.length) { + while (asyncQueue.length) { try { asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression); + asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); } catch (e) { - clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; @@ -22035,20 +23407,20 @@ function $RootScopeProvider(){ if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' + : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; + watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); + watchLog[logIdx].push({ + msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, + newVal: value, + oldVal: last + }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers @@ -22058,7 +23430,6 @@ function $RootScopeProvider(){ } } } catch (e) { - clearPhase(); $exceptionHandler(e); } } @@ -22069,7 +23440,7 @@ function $RootScopeProvider(){ // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { + while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } @@ -22077,19 +23448,19 @@ function $RootScopeProvider(){ // `break traverseScopesLoop;` takes us to here - if((dirty || asyncQueue.length) && !(ttl--)) { + if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); + TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); - while(postDigestQueue.length) { + while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { @@ -22114,7 +23485,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$destroy - * @function + * @kind function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies @@ -22142,7 +23513,9 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + 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) @@ -22151,6 +23524,10 @@ function $RootScopeProvider(){ if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + // Disable listeners, watchers and apply/digest methods + this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; + 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. @@ -22161,21 +23538,13 @@ function $RootScopeProvider(){ // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = null; - - // don't reset these to null in case some async task tries to register a listener/watch/task - this.$$listeners = {}; - this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; - - // prevent NPEs since these methods have references to properties we nulled out - this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = function() { return noop; }; + this.$$childTail = this.$root = this.$$watchers = null; }, /** * @ngdoc method * @name $rootScope.Scope#$eval - * @function + * @kind function * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in @@ -22207,7 +23576,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$evalAsync - * @function + * @kind function * * @description * Executes the expression on the current scope at a later point in time. @@ -22232,29 +23601,30 @@ function $RootScopeProvider(){ * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. */ - $evalAsync: function(expr) { + $evalAsync: function(expr, locals) { // if we are outside of an $digest loop and this is the first time we are scheduling async // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + if (!$rootScope.$$phase && !asyncQueue.length) { $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { + if (asyncQueue.length) { $rootScope.$digest(); } }); } - this.$$asyncQueue.push({scope: this, expression: expr}); + asyncQueue.push({scope: this, expression: expr, locals: locals}); }, - $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); + $$postDigest: function(fn) { + postDigestQueue.push(fn); }, /** * @ngdoc method * @name $rootScope.Scope#$apply - * @function + * @kind function * * @description * `$apply()` is used to execute an expression in angular from outside of the angular @@ -22315,8 +23685,35 @@ function $RootScopeProvider(){ /** * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function + * + * @description + * Schedule the invokation of $apply to occur at a later time. The actual time difference + * varies across browsers, but is typically around ~10 milliseconds. + * + * This can be used to queue up multiple expressions which need to be evaluated in the same + * digest. + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + */ + $applyAsync: function(expr) { + var scope = this; + expr && applyAsyncQueue.push($applyAsyncExpression); + scheduleApplyAsync(); + + function $applyAsyncExpression() { + scope.$eval(expr); + } + }, + + /** + * @ngdoc method * @name $rootScope.Scope#$on - * @function + * @kind function * * @description * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for @@ -22327,7 +23724,8 @@ function $RootScopeProvider(){ * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the + * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel * further event propagation (available only for events that were `$emit`-ed). @@ -22356,8 +23754,11 @@ function $RootScopeProvider(){ var self = this; return function() { - namedListeners[indexOf(namedListeners, listener)] = null; - decrementListenerCount(self, 1, name); + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; + decrementListenerCount(self, 1, name); + } }; }, @@ -22365,7 +23766,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$emit - * @function + * @kind function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the @@ -22404,7 +23805,7 @@ function $RootScopeProvider(){ do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; - for (i=0, length=namedListeners.length; i<length; i++) { + for (i = 0, length = namedListeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!namedListeners[i]) { @@ -22421,11 +23822,16 @@ function $RootScopeProvider(){ } } //if any listener on the current scope stops propagation, prevent bubbling - if (stopPropagation) return event; + if (stopPropagation) { + event.currentScope = null; + return event; + } //traverse upwards scope = scope.$parent; } while (scope); + event.currentScope = null; + return event; }, @@ -22433,7 +23839,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$broadcast - * @function + * @kind function * * @description * Dispatches an event `name` downwards to all child scopes (and their children) notifying the @@ -22462,15 +23868,18 @@ function $RootScopeProvider(){ event.defaultPrevented = true; }, defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; - for (i=0, length = listeners.length; i<length; i++) { + for (i = 0, length = listeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!listeners[i]) { listeners.splice(i, 1); @@ -22481,7 +23890,7 @@ function $RootScopeProvider(){ try { listeners[i].apply(null, listenerArgs); - } catch(e) { + } catch (e) { $exceptionHandler(e); } } @@ -22492,18 +23901,24 @@ function $RootScopeProvider(){ // (though it differs due to having the extra check for $$listenerCount) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { + while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } + event.currentScope = null; return event; } }; var $rootScope = new Scope(); + //The internal queues. Expose them on the $rootScope for debugging/testing purposes. + var asyncQueue = $rootScope.$$asyncQueue = []; + var postDigestQueue = $rootScope.$$postDigestQueue = []; + var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + return $rootScope; @@ -22519,11 +23934,6 @@ function $RootScopeProvider(){ $rootScope.$$phase = null; } - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } function decrementListenerCount(current, count, name) { do { @@ -22540,6 +23950,25 @@ function $RootScopeProvider(){ * because it's unique we can easily tell it apart from other values */ function initWatchVal() {} + + function flushApplyAsync() { + while (applyAsyncQueue.length) { + try { + applyAsyncQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + applyAsyncId = null; + } + + function scheduleApplyAsync() { + if (applyAsyncId === null) { + applyAsyncId = $browser.defer(function() { + $rootScope.$apply(flushApplyAsync); + }); + } + } }]; } @@ -22549,7 +23978,7 @@ function $RootScopeProvider(){ */ function $$SanitizeUriProvider() { var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description @@ -22604,12 +24033,9 @@ function $$SanitizeUriProvider() { return function sanitizeUri(uri, isImage) { var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; var normalizedVal; - // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. - if (!msie || msie >= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:' + normalizedVal; } return uri; }; @@ -22630,15 +24056,6 @@ var SCE_CONTEXTS = { // Helper functions follow. -// Copied from: -// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 -// Prereq: s is a string. -function escapeForRegexp(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). - replace(/\x08/g, '\\x08'); -} - - function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -22681,7 +24098,7 @@ function adjustMatchers(matchers) { /** * @ngdoc service * @name $sceDelegate - * @function + * @kind function * * @description * @@ -22728,19 +24145,21 @@ function adjustMatchers(matchers) { * * Here is what a secure configuration for this scenario might look like: * - * <pre class="prettyprint"> - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**']); - * - * // The blacklist overrides the whitelist so the open redirect here is blocked. - * $sceDelegateProvider.resourceUrlBlacklist([ - * 'http://myapp.example.com/clickThru**']); - * }); - * </pre> + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` */ function $SceDelegateProvider() { @@ -22753,7 +24172,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlWhitelist - * @function + * @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 @@ -22772,7 +24191,7 @@ function $SceDelegateProvider() { * @description * Sets/Gets the whitelist of trusted resource URLs. */ - this.resourceUrlWhitelist = function (value) { + this.resourceUrlWhitelist = function(value) { if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value); } @@ -22782,7 +24201,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlBlacklist - * @function + * @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 @@ -22806,7 +24225,7 @@ function $SceDelegateProvider() { * Sets/Gets the blacklist of trusted resource URLs. */ - this.resourceUrlBlacklist = function (value) { + this.resourceUrlBlacklist = function(value) { if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value); } @@ -23009,7 +24428,7 @@ function $SceDelegateProvider() { /** * @ngdoc service * @name $sce - * @function + * @kind function * * @description * @@ -23024,7 +24443,7 @@ function $SceDelegateProvider() { * * As of version 1.2, Angular ships with SCE enabled by default. * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow * one to execute arbitrary javascript by the use of the expression() syntax. Refer * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them. * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>` @@ -23035,10 +24454,10 @@ function $SceDelegateProvider() { * * Here's an example of a binding in a privileged context: * - * <pre class="prettyprint"> - * <input ng-model="userHtml"> - * <div ng-bind-html="userHtml"> - * </pre> + * ``` + * <input ng-model="userHtml"> + * <div ng-bind-html="userHtml"></div> + * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE * disabled, this application allows the user to render arbitrary HTML into the DIV. @@ -23071,22 +24490,22 @@ function $SceDelegateProvider() { * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * - * <pre class="prettyprint"> - * var ngBindHtmlDirective = ['$sce', function($sce) { - * return function(scope, element, attr) { - * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { - * element.html(value || ''); - * }); - * }; - * }]; - * </pre> + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` * * ## Impact on loading templates * @@ -23108,7 +24527,7 @@ function $SceDelegateProvider() { * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead for the developer? + * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * @@ -23135,7 +24554,7 @@ function $SceDelegateProvider() { * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. | * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | @@ -23159,7 +24578,7 @@ function $SceDelegateProvider() { * - `**`: matches zero or more occurrences of *any* character. As such, it's not * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). * - **RegExp** (*see caveat below*) * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax @@ -23190,66 +24609,65 @@ function $SceDelegateProvider() { * * ## Show me an example using SCE. * - * @example -<example module="mySceApp" deps="angular-sanitize.js"> -<file name="index.html"> - <div ng-controller="myAppController as myCtrl"> - <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> - <b>User comments</b><br> - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. - <div class="well"> - <div ng-repeat="userComment in myCtrl.userComments"> - <b>{{userComment.name}}</b>: - <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> - <br> - </div> - </div> - </div> -</file> - -<file name="script.js"> - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + - 'sanitization."">Hover over this text.</span>'); - }); -</file> - -<file name="test_data.json"> -[ - { "name": "Alice", - "htmlComment": - "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" - }, - { "name": "Bob", - "htmlComment": "<i>Yes!</i> Am I the only other one?" - } -] -</file> - -<file name="protractor.js" type="protractor"> - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('<span>Is <i>anyone</i> reading this?</span>'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + - 'sanitization."">Hover over this text.</span>'); - }); - }); -</file> -</example> + * <example module="mySceApp" deps="angular-sanitize.js"> + * <file name="index.html"> + * <div ng-controller="AppController as myCtrl"> + * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> + * <b>User comments</b><br> + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + * <div class="well"> + * <div ng-repeat="userComment in myCtrl.userComments"> + * <b>{{userComment.name}}</b>: + * <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> + * <br> + * </div> + * </div> + * </div> + * </file> + * + * <file name="script.js"> + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + + * 'sanitization."">Hover over this text.</span>'); + * }]); + * </file> + * + * <file name="test_data.json"> + * [ + * { "name": "Alice", + * "htmlComment": + * "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" + * }, + * { "name": "Bob", + * "htmlComment": "<i>Yes!</i> Am I the only other one?" + * } + * ] + * </file> + * + * <file name="protractor.js" type="protractor"> + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * .toBe('<span>Is <i>anyone</i> reading this?</span>'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + + * 'sanitization."">Hover over this text.</span>'); + * }); + * }); + * </file> + * </example> * * * @@ -23263,13 +24681,13 @@ function $SceDelegateProvider() { * * That said, here's how you can completely disable SCE: * - * <pre class="prettyprint"> - * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { - * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. - * $sceProvider.enabled(false); - * }); - * </pre> + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects. + * $sceProvider.enabled(false); + * }); + * ``` * */ /* jshint maxlen: 100 */ @@ -23280,7 +24698,7 @@ function $SceProvider() { /** * @ngdoc method * @name $sceProvider#enabled - * @function + * @kind function * * @param {boolean=} value If provided, then enables/disables SCE. * @return {boolean} true if SCE is enabled, false otherwise. @@ -23288,7 +24706,7 @@ function $SceProvider() { * @description * Enables/disables SCE and returns the current value. */ - this.enabled = function (value) { + this.enabled = function(value) { if (arguments.length) { enabled = !!value; } @@ -23342,23 +24760,23 @@ function $SceProvider() { * sce.js and sceSpecs.js would need to be aware of this detail. */ - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + if (enabled && msie < 8) { throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + '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.'); } - var sce = copy(SCE_CONTEXTS); + var sce = shallowCopy(SCE_CONTEXTS); /** * @ngdoc method * @name $sce#isEnabled - * @function + * @kind function * * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. @@ -23366,7 +24784,7 @@ function $SceProvider() { * @description * Returns a boolean indicating if SCE is enabled. */ - sce.isEnabled = function () { + sce.isEnabled = function() { return enabled; }; sce.trustAs = $sceDelegate.trustAs; @@ -23380,7 +24798,7 @@ function $SceProvider() { /** * @ngdoc method - * @name $sce#parse + * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link @@ -23402,9 +24820,9 @@ function $SceProvider() { if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + return $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); } }; @@ -23571,7 +24989,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -23588,7 +25006,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -23605,7 +25023,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -23622,7 +25040,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -23639,7 +25057,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -23655,15 +25073,15 @@ function $SceProvider() { getTrusted = sce.getTrusted, trustAs = sce.trustAs; - forEach(SCE_CONTEXTS, function (enumValue, name) { + forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { + sce[camelCase("parse_as_" + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function (value) { + sce[camelCase("get_trusted_" + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function (value) { + sce[camelCase("trust_as_" + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -23680,7 +25098,6 @@ function $SceProvider() { * @requires $document * * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * @@ -23694,31 +25111,30 @@ function $SnifferProvider() { int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - documentMode = document.documentMode, vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, animations = false, match; if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { + for (var prop in bodyStyle) { + if (match = vendorRegex.exec(prop)) { vendorPrefix = match[0]; vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); break; } } - if(!vendorPrefix) { + if (!vendorPrefix) { vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; } transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - if (android && (!transitions||!animations)) { + if (android && (!transitions || !animations)) { transitions = isString(document.body.style.webkitTransition); animations = isString(document.body.style.webkitAnimation); } @@ -23737,14 +25153,13 @@ function $SnifferProvider() { // jshint -W018 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + // IE10+ implements 'input' event but it erroneously fires under various situations, + // e.g. when placeholder changes, or a form is focused. + if (event === 'input' && msie <= 11) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -23755,18 +25170,192 @@ function $SnifferProvider() { }, csp: csp(), vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode + transitions: transitions, + animations: animations, + android: android }; }]; } +var $compileMinErr = minErr('$compile'); + +/** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service downloads the provided template using `$http` and, upon success, + * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data + * of the HTTP request is empty then a `$compile` error will be thrown (the exception can be thwarted + * by setting the 2nd parameter of the function to true). + * + * @param {string} 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. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ +function $TemplateRequestProvider() { + this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { + function handleRequestFn(tpl, ignoreRequestError) { + var self = handleRequestFn; + self.totalPendingRequests++; + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + var httpOptions = { + cache: $templateCache, + transformResponse: transformResponse + }; + + return $http.get(tpl, httpOptions) + .then(function(response) { + self.totalPendingRequests--; + return response.data; + }, handleError); + + function handleError(resp) { + self.totalPendingRequests--; + if (!ignoreRequestError) { + throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); + } + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + }]; +} + +function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) != -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location.url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); + }; + + return testability; + }]; +} + function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; @@ -23796,15 +25385,15 @@ function $TimeoutProvider() { * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), + var skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { try { deferred.resolve(fn()); - } catch(e) { + } catch (e) { deferred.reject(e); $exceptionHandler(e); } @@ -23855,7 +25444,7 @@ function $TimeoutProvider() { // exactly the behavior needed here. There is little value is mocking these out for this // service. var urlParsingNode = document.createElement("a"); -var originUrl = urlResolve(window.location.href, true); +var originUrl = urlResolve(window.location.href); /** @@ -23893,7 +25482,7 @@ var originUrl = urlResolve(window.location.href, true); * https://github.com/angular/angular.js/pull/2902 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * - * @function + * @kind function * @param {string} url The URL to be parsed. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. @@ -23910,7 +25499,7 @@ var originUrl = urlResolve(window.location.href, true); * | pathname | The pathname, beginning with "/" * */ -function urlResolve(url, base) { +function urlResolve(url) { var href = url; if (msie) { @@ -23966,17 +25555,18 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - <example> + <example module="windowExample"> <file name="index.html"> <script> - function Ctrl($scope, $window) { - $scope.greeting = 'Hello, World!'; - $scope.doGreeting = function(greeting) { + angular.module('windowExample', []) + .controller('ExampleController', ['$scope', '$window', function($scope, $window) { + $scope.greeting = 'Hello, World!'; + $scope.doGreeting = function(greeting) { $window.alert(greeting); - }; - } + }; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="text" ng-model="greeting" /> <button ng-click="doGreeting(greeting)">ALERT</button> </div> @@ -23990,10 +25580,21 @@ function urlIsSameOrigin(requestUrl) { </file> </example> */ -function $WindowProvider(){ +function $WindowProvider() { this.$get = valueFn(window); } +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + /** * @ngdoc provider * @name $filterProvider @@ -24043,21 +25644,11 @@ function $WindowProvider(){ * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ -/** - * @ngdoc method - * @name $filterProvider#register - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {Function} fn The filter factory function which is injectable. - */ - /** * @ngdoc service * @name $filter - * @function + * @kind function * @description * Filters are used for formatting data displayed to the user. * @@ -24067,21 +25658,38 @@ function $WindowProvider(){ * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function - */ + * @example + <example name="$filter" module="filterExample"> + <file name="index.html"> + <div ng-controller="MainCtrl"> + <h3>{{ originalText }}</h3> + <h3>{{ filteredText }}</h3> + </div> + </file> + + <file name="script.js"> + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + </file> + </example> + */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; /** * @ngdoc method - * @name $controllerProvider#register + * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ function register(name, factory) { - if(isObject(name)) { + if (isObject(name)) { var filters = {}; forEach(name, function(filter, key) { filters[key] = register(key, filter); @@ -24127,7 +25735,7 @@ function $FilterProvider($provide) { /** * @ngdoc filter * @name filter - * @function + * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. @@ -24147,11 +25755,13 @@ function $FilterProvider($provide) { * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * as described above. The predicate can be negated by prefixing the string with `!`. + * For Example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". * - * - `function(value)`: 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)`: 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. * * @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 @@ -24159,15 +25769,15 @@ function $FilterProvider($provide) { * * Can be one of: * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if the item should be included in filtered result. * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. + * this is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. * * @example <example> @@ -24241,112 +25851,109 @@ function filterFilter() { return function(array, expression, comparator) { if (!isArray(array)) return array; - var comparatorType = typeof(comparator), - predicates = []; - - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; - } else { - comparator = function(obj, text) { - if (obj && text && typeof obj === 'object' && typeof text === 'object') { - for (var objKey in obj) { - if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && - comparator(obj[objKey], text[objKey])) { - return true; - } - } - return false; - } - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; - } - } + var predicateFn; + var matchAgainstAnyProp; - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : (value && value[path]), expression[path]); - }); - })(key); - } - break; case 'function': - predicates.push(expression); + predicateFn = expression; + break; + case 'boolean': + case 'number': + case 'string': + matchAgainstAnyProp = true; + //jshint -W086 + case 'object': + //jshint +W086 + predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); break; default: return array; } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); + + return array.filter(predicateFn); + }; +} + +// Helper functions for `filterFilter` +function createPredicateFn(expression, comparator, matchAgainstAnyProp) { + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isObject(actual) || isObject(expected)) { + // Prevent an object to be considered equal to a string like `'[object'` + return false; } - } - return filtered; + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + return deepCompare(item, expression, comparator, matchAgainstAnyProp); }; + + return predicateFn; +} + +function deepCompare(actual, expected, comparator, matchAgainstAnyProp) { + var actualType = typeof actual; + var expectedType = typeof expected; + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); + } else if (actualType === 'array') { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator)) { + return true; + } + } + return false; + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal)) { + continue; + } + + var keyIsDollar = key === '$'; + var actualVal = keyIsDollar ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, keyIsDollar)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + break; + case 'function': + return false; + default: + return comparator(actual, expected); + } } /** * @ngdoc filter * @name currency - * @function + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -24354,27 +25961,31 @@ function filterFilter() { * * @param {number} amount Input to filter. * @param {string=} symbol Currency symbol or identifier to be displayed. + * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale * @returns {string} Formatted number. * * * @example - <example> + <example module="currencyExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.amount = 1234.56; - } + angular.module('currencyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.amount = 1234.56; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="number" ng-model="amount"> <br> default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> - custom currency identifier (USD$): <span>{{amount | currency:"USD$"}}</span> + 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> </div> </file> <file name="protractor.js" type="protractor"> it('should init with 1234.56', function() { expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { if (browser.params.browser == 'safari') { @@ -24385,7 +25996,8 @@ function filterFilter() { 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.binding('amount | currency:"USD$"')).getText()).toBe('(USD$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> @@ -24393,17 +26005,27 @@ function filterFilter() { currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); + return function(amount, currencySymbol, fractionSize) { + if (isUndefined(currencySymbol)) { + currencySymbol = formats.CURRENCY_SYM; + } + + if (isUndefined(fractionSize)) { + fractionSize = formats.PATTERNS[1].maxFrac; + } + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). + replace(/\u00A4/g, currencySymbol); }; } /** * @ngdoc filter * @name number - * @function + * @kind function * * @description * Formats a number as text. @@ -24417,14 +26039,15 @@ function currencyFilter($locale) { * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - <example> + <example module="numberFilterExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = 1234.56789; - } + angular.module('numberFilterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = 1234.56789; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter number: <input ng-model='val'><br> Default formatting: <span id='number-default'>{{val | number}}</span><br> No fractions: <span>{{val | number:0}}</span><br> @@ -24454,14 +26077,18 @@ numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); }; } var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (number == null || !isFinite(number) || isObject(number)) return ''; + if (!isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -24473,7 +26100,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (numStr.indexOf('e') !== -1) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; + number = 0; } else { formatedText = numStr; hasExponent = true; @@ -24488,8 +26115,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; + // 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); + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; @@ -24501,7 +26131,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { + if ((pos - i) % group === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); @@ -24509,28 +26139,32 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { + if ((whole.length - i) % lgroup === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); } // format fraction part. - while(fraction.length < fractionSize) { + while (fraction.length < fractionSize) { fraction += '0'; } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); } else { - - if (fractionSize > 0 && number > -1 && number < 1) { + if (fractionSize > 0 && number < 1) { formatedText = number.toFixed(fractionSize); + number = parseFloat(formatedText); } } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + if (number === 0) { + isNegative = false; + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre, + formatedText, + isNegative ? pattern.negSuf : pattern.posSuf); return parts.join(''); } @@ -24541,7 +26175,7 @@ function padNumber(num, digits, trim) { num = -num; } num = '' + num; - while(num.length < digits) num = '0' + num; + while (num.length < digits) num = '0' + num; if (trim) num = num.substr(num.length - digits); return neg + num; @@ -24554,7 +26188,7 @@ function dateGetter(name, size, offset, trim) { var value = date['get' + name](); if (offset > 0 || value > -offset) value += offset; - if (value === 0 && offset == -12 ) value = 12; + if (value === 0 && offset == -12) value = 12; return padNumber(value, size, trim); }; } @@ -24578,6 +26212,32 @@ function timeZoneGetter(date) { return paddedZone; } +function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); +} + +function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); +} + +function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; +} + function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } @@ -24606,16 +26266,18 @@ var DATE_FORMATS = { EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, - Z: timeZoneGetter + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1) }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter * @name date - * @function + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -24635,40 +26297,44 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker + * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year + * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * 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. + * 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. * * @example @@ -24680,6 +26346,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br> + <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>: + <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br> </file> <file name="protractor.js" type="protractor"> it('should format date', function() { @@ -24689,6 +26357,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); }); </file> </example> @@ -24713,10 +26383,10 @@ function dateFilter($locale) { tzMin = int(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); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + var h = int(match[4] || 0) - tzHour; + var m = int(match[5] || 0) - tzMin; + var s = int(match[6] || 0); + var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } @@ -24724,7 +26394,7 @@ function dateFilter($locale) { } - return function(date, format) { + return function(date, format, timezone) { var text = '', parts = [], fn, match; @@ -24732,11 +26402,7 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { @@ -24747,7 +26413,7 @@ function dateFilter($locale) { return date; } - while(format) { + while (format) { match = DATE_FORMATS_SPLIT.exec(format); if (match) { parts = concat(parts, match, 1); @@ -24758,7 +26424,11 @@ function dateFilter($locale) { } } - forEach(parts, function(value){ + if (timezone && timezone === 'UTC') { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); + } + forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS) : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); @@ -24772,7 +26442,7 @@ function dateFilter($locale) { /** * @ngdoc filter * @name json - * @function + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -24781,25 +26451,31 @@ function dateFilter($locale) { * the binding is automatically converted to JSON. * * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. * @returns {string} JSON string. * * * @example <example> <file name="index.html"> - <pre>{{ {'name':'value'} | json }}</pre> + <pre id="default-spacing">{{ {'name':'value'} | json }}</pre> + <pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre> </file> <file name="protractor.js" type="protractor"> it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); </file> </example> * */ function jsonFilter() { - return function(object) { - return toJson(object, true); + return function(object, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); }; } @@ -24807,7 +26483,7 @@ function jsonFilter() { /** * @ngdoc filter * @name lowercase - * @function + * @kind function * @description * Converts string to lowercase. * @see angular.lowercase @@ -24818,7 +26494,7 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter * @name uppercase - * @function + * @kind function * @description * Converts string to uppercase. * @see angular.uppercase @@ -24828,14 +26504,15 @@ var uppercaseFilter = valueFn(uppercase); /** * @ngdoc filter * @name limitTo - * @function + * @kind function * * @description * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. + * are taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. If a number is used as input, it is + * converted to a string. * - * @param {Array|string} input Source array or string to be limited. + * @param {Array|string|number} input Source array, string or number to be limited. * @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 @@ -24844,61 +26521,82 @@ var uppercaseFilter = valueFn(uppercase); * had less than `limit` elements. * * @example - <example> + <example module="limitToExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.numbers = [1,2,3,4,5,6,7,8,9]; - $scope.letters = "abcdefghi"; - $scope.numLimit = 3; - $scope.letterLimit = 3; - } + angular.module('limitToExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.numbers = [1,2,3,4,5,6,7,8,9]; + $scope.letters = "abcdefghi"; + $scope.longNumber = 2345432342; + $scope.numLimit = 3; + $scope.letterLimit = 3; + $scope.longNumberLimit = 3; + }]); </script> - <div ng-controller="Ctrl"> - Limit {{numbers}} to: <input type="integer" ng-model="numLimit"> + <div ng-controller="ExampleController"> + Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit"> <p>Output numbers: {{ numbers | limitTo:numLimit }}</p> - Limit {{letters}} to: <input type="integer" ng-model="letterLimit"> + Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit"> <p>Output letters: {{ letters | limitTo:letterLimit }}</p> + Limit {{longNumber}} to: <input type="number" step="1" ng-model="longNumberLimit"> + <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p> </div> </file> <file name="protractor.js" type="protractor"> var numLimitInput = element(by.model('numLimit')); var letterLimitInput = element(by.model('letterLimit')); + var longNumberLimitInput = element(by.model('longNumberLimit')); var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); it('should limit the number array to first three items', function() { expect(numLimitInput.getAttribute('value')).toBe('3'); expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(longNumberLimitInput.getAttribute('value')).toBe('3'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); expect(limitedLetters.getText()).toEqual('Output letters: abc'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); }); - it('should update the output when -3 is entered', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('-3'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('-3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - }); + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // longNumberLimitInput.clear(); + // longNumberLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); + // }); it('should not exceed the maximum size of input array', function() { numLimitInput.clear(); numLimitInput.sendKeys('100'); letterLimitInput.clear(); letterLimitInput.sendKeys('100'); + longNumberLimitInput.clear(); + longNumberLimitInput.sendKeys('100'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); }); </file> </example> - */ -function limitToFilter(){ +*/ +function limitToFilter() { return function(input, limit) { + if (isNumber(input)) input = input.toString(); if (!isArray(input) && !isString(input)) return input; - limit = int(limit); + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = int(limit); + } if (isString(input)) { //NaN check on limit @@ -24926,7 +26624,7 @@ function limitToFilter(){ n = input.length; } - for (; i<n; i++) { + for (; i < n; i++) { out.push(input[i]); } @@ -24937,43 +26635,53 @@ function limitToFilter(){ /** * @ngdoc filter * @name orderBy - * @function + * @kind function * * @description - * Orders a specified `array` by the `expression` predicate. + * 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. * * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). + * - `string`: An Angular expression. The result of this expression is used to compare elements + * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by + * 3 first characters of a property called `name`). The result of a constant expression + * is interpreted as a property name to be used in comparisons (for example `"special name"` + * to sort object by the value of their `special name` property). An expression can be + * optionally prefixed with `+` or `-` to control ascending or descending sort order + * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array + * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * + * If the predicate is missing or empty then it defaults to `'+'`. + * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - <example> + <example module="orderByExample"> <file name="index.html"> <script> - function Ctrl($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'; - } + 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="Ctrl"> + <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> [ <a href="" ng-click="predicate=''">unsorted</a> ] @@ -24993,53 +26701,131 @@ function limitToFilter(){ </div> </file> </example> + * + * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the + * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the + * desired parameters. + * + * Example: + * + * @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> + </file> + + <file name="script.js"> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { + var orderBy = $filter('orderBy'); + $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.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + }]); + </file> +</example> */ orderByFilter.$inject = ['$parse']; -function orderByFilter($parse){ +function orderByFilter($parse) { return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ + if (!(isArrayLike(array))) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; + if (sortPredicate.length === 0) { sortPredicate = ['+']; } + sortPredicate = sortPredicate.map(function(predicate) { var descending = false, get = predicate || identity; if (isString(predicate)) { if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } + if (predicate === '') { + // Effectively no predicate was passed so we compare identity + return reverseComparator(function(a, b) { + return compare(a, b); + }, descending); + } get = $parse(predicate); if (get.constant) { var key = get(); - return reverseComparator(function(a,b) { + return reverseComparator(function(a, b) { return compare(a[key], b[key]); }, descending); } } - return reverseComparator(function(a,b){ + return reverseComparator(function(a, b) { return compare(get(a),get(b)); }, descending); }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { + 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 toBoolean(descending) - ? function(a,b){return comp(b,a);} + return descending + ? function(a, b) {return comp(b,a);} : comp; } - function compare(v1, v2){ + + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; + } + } + + function objectToString(value) { + if (value === null) return 'null'; + if (typeof value.toString === 'function') { + value = value.toString(); + if (isPrimitive(value)) return value; + } + if (typeof value.valueOf === 'function') { + value = value.valueOf(); + if (isPrimitive(value)) return value; + } + return ''; + } + + function compare(v1, v2) { var t1 = typeof v1; var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { + if (t1 === t2 && t1 === "object") { + v1 = objectToString(v1); + v2 = objectToString(v2); + } + if (t1 === t2) { + if (t1 === "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); } @@ -25078,28 +26864,12 @@ function ngDirective(directive) { var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - - if (msie <= 8) { - - // turn <a href ng-click="..">link</a> into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - if (!attr.href && !attr.xlinkHref && !attr.name) { return function(scope, element) { // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? 'xlink:href' : 'href'; - element.on('click', function(event){ + element.on('click', function(event) { // if we have no href url, then don't navigate anywhere. if (!element.attr(href)) { event.preventDefault(); @@ -25121,18 +26891,17 @@ var htmlAnchorDirective = valueFn({ * make the link go to the wrong URL if the user clicks it before * Angular has a chance to replace the `{{hash}}` markup with its * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. + * and will most likely return a 404 error. The `ngHref` directive + * solves this problem. * * The wrong way to write it: * ```html - * <a href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * The correct way to write it: * ```html - * <a ng-href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * @element A @@ -25176,7 +26945,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/123$/); }); - }, 1000, 'page should navigate to /123'); + }, 5000, 'page should navigate to /123'); }); xit('should execute ng-click but not reload when href empty string and name specified', function() { @@ -25204,7 +26973,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/6$/); }); - }, 1000, 'page should navigate to /6'); + }, 5000, 'page should navigate to /6'); }); </file> </example> @@ -25270,7 +27039,7 @@ var htmlAnchorDirective = valueFn({ * * @description * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: * ```html * <div ng-init="scope = { isDisabled: false }"> * <button disabled="{{scope.isDisabled}}">Disabled</button> @@ -25461,6 +27230,7 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { + restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -25471,6 +27241,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }; }); +// aliased input attrs are evaluated +forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set("ngPattern", new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; +}); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { @@ -25490,8 +27283,12 @@ forEach(['src', 'srcset', 'href'], function(attrName) { } attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } attr.$set(name, value); @@ -25506,14 +27303,22 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl */ +/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true + */ var nullFormCtrl = { $addControl: noop, + $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, - $setPristine: noop -}; + $setPristine: noop, + $setSubmitted: noop +}, +SUBMITTED_CLASS = 'ng-submitted'; + +function nullFormRenameControl(control, name) { + control.$name = name; +} /** * @ngdoc type @@ -25523,13 +27328,13 @@ var nullFormCtrl = { * @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} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: + * @property {Object} $error Is an object hash, containing references to controls or + * forms with failing validators, where: * * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * + * - values are arrays of controls or forms that have a failing validator for given error name. * * Built-in validation tokens: * @@ -25542,9 +27347,14 @@ var nullFormCtrl = { * - `pattern` * - `required` * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` * * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, + * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance @@ -25552,33 +27362,59 @@ var nullFormCtrl = { * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; -function FormController(element, attrs, $scope, $animate) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; +function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, controls = []; + var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + // init state - form.$name = attrs.name || attrs.ngForm; + form.$error = {}; + form.$$success = {}; + form.$pending = undefined; + form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); form.$dirty = false; form.$pristine = true; form.$valid = true; form.$invalid = false; + form.$submitted = false; parentForm.$addControl(form); - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - toggleValidCss(true); + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + form.$rollbackViewValue = function() { + forEach(controls, function(control) { + control.$rollbackViewValue(); + }); + }; - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + form.$commitViewValue = function() { + forEach(controls, function(control) { + control.$commitViewValue(); + }); + }; /** * @ngdoc method @@ -25600,6 +27436,17 @@ function FormController(element, attrs, $scope, $animate) { } }; + // Private API: rename a form control + form.$$renameControl = function(control, newName) { + var oldName = control.$name; + + if (form[oldName] === control) { + delete form[oldName]; + } + form[newName] = control; + control.$name = newName; + }; + /** * @ngdoc method * @name form.FormController#$removeControl @@ -25613,13 +27460,17 @@ function FormController(element, attrs, $scope, $animate) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); + forEach(form.$pending, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$error, function(value, name) { + form.$setValidity(name, null, control); }); arrayRemove(controls, control); }; + /** * @ngdoc method * @name form.FormController#$setValidity @@ -25629,43 +27480,33 @@ function FormController(element, attrs, $scope, $animate) { * * This method will also propagate to parent forms. */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); + addSetValidityMethod({ + ctrl: this, + $element: element, + set: function(object, property, control) { + var list = object[property]; + if (!list) { + object[property] = [control]; + } else { + var index = list.indexOf(control); + if (index === -1) { + list.push(control); } } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); + }, + unset: function(object, property, control) { + var list = object[property]; + if (!list) { + return; } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); + arrayRemove(list, control); + if (list.length === 0) { + delete object[property]; } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; + }, + parentForm: parentForm, + $animate: $animate + }); /** * @ngdoc method @@ -25699,17 +27540,48 @@ function FormController(element, attrs, $scope, $animate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function () { - $animate.removeClass(element, DIRTY_CLASS); - $animate.addClass(element, PRISTINE_CLASS); + form.$setPristine = function() { + $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; + form.$submitted = false; forEach(controls, function(control) { control.$setPristine(); }); }; -} + /** + * @ngdoc method + * @name form.FormController#$setUntouched + * + * @description + * Sets the form to its untouched state. + * + * This method can be called to remove the 'ng-touched' class and set the form controls to their + * untouched state (ng-untouched class). + * + * Setting a form controls back to their untouched state is often useful when setting the form + * back to its pristine state. + */ + form.$setUntouched = function() { + forEach(controls, function(control) { + control.$setUntouched(); + }); + }; + + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + form.$setSubmitted = function() { + $animate.addClass(element, SUBMITTED_CLASS); + form.$submitted = true; + parentForm.$setSubmitted(); + }; +} /** * @ngdoc directive @@ -25759,6 +27631,7 @@ function FormController(element, attrs, $scope, $animate) { * - `ng-invalid` is set if the form is invalid. * - `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. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -25792,8 +27665,9 @@ function FormController(element, attrs, $scope, $animate) { * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. * * ## Animation Hooks * @@ -25820,12 +27694,13 @@ function FormController(element, attrs, $scope, $animate) { * </pre> * * @example - <example deps="angular-animate.js" animations="true" fixBase="true"> + <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.userType = 'guest'; - } + angular.module('formExample', []) + .controller('FormController', ['$scope', function($scope) { + $scope.userType = 'guest'; + }]); </script> <style> .my-form { @@ -25837,7 +27712,7 @@ function FormController(element, attrs, $scope, $animate) { background: red; } </style> - <form name="myForm" ng-controller="Ctrl" class="my-form"> + <form name="myForm" ng-controller="FormController" class="my-form"> userType: <input name="input" ng-model="userType" required> <span class="error" ng-show="myForm.input.$error.required">Required!</span><br> <tt>userType = {{userType}}</tt><br> @@ -25870,6 +27745,8 @@ function FormController(element, attrs, $scope, $animate) { </file> </example> * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { @@ -25877,48 +27754,60 @@ var formDirectiveFactory = function(isNgForm) { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, - compile: function() { + compile: function ngFormCompile(formElement) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { + pre: function ngFormPreLink(scope, formElement, attr, controller) { + // 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 // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault - ? event.preventDefault() - : event.returnValue = false; // IE + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + addEventListenerFn(formElement[0], 'submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); }, 0, false); }); } - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; + var parentFormCtrl = controller.$$parentForm, + alias = controller.$name; if (alias) { setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { + if (alias === newValue) return; + setter(scope, alias, undefined, alias); + alias = newValue; + setter(scope, alias, controller, alias); + parentFormCtrl.$$renameControl(controller, alias); }); } + formElement.on('$destroy', function() { + parentFormCtrl.$removeControl(controller); + if (alias) { + setter(scope, alias, undefined, alias); + } + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); } }; } @@ -25931,17 +27820,27 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: 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#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/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 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 DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + +var $ngModelMinErr = new minErr('ngModel'); var inputType = { @@ -25950,7 +27849,8 @@ var inputType = { * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` 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. @@ -25961,24 +27861,33 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @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. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - <example name="text-input-directive"> + <example name="text-input-directive" module="textInputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'guest'; - $scope.word = /^\s*\w*\s*$/; - } + angular.module('textInputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'guest'; + $scope.word = /^\s*\w*\s*$/; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Single word: <input type="text" name="input" ng-model="text" ng-pattern="word" required ng-trim="false"> <span class="error" ng-show="myForm.input.$error.required"> @@ -26022,6 +27931,463 @@ var inputType = { */ 'text': textInputType, + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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). + * @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). + * @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 input changes due to user + * interaction with the input element. + * + * @example + <example name="date-input-directive" module="dateInputExample"> + <file name="index.html"> + <script> + angular.module('dateInputExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 22); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date in 2013: + <input type="date" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), + + /** + * @ngdoc input + * @name input[datetime-local] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="datetimelocal-input-directive" module="dateExample"> + <file name="index.html"> + <script> + angular.module('dateExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2010, 11, 28, 14, 57); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input type="datetime-local" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[time] + * + * @description + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="time-input-directive" module="timeExample"> + <file name="index.html"> + <script> + angular.module('timeExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(1970, 0, 1, 14, 57, 0); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a between 8am and 5pm: + <input type="time" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="week-input-directive" module="weekExample"> + <file name="index.html"> + <script> + angular.module('weekExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 0, 3); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input id="exampleInput" type="week" name="input" ng-model="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> + <tt>value = {{value | date: "yyyy-Www"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), + + /** + * @ngdoc input + * @name input[month] + * + * @description + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * If the model is not set to the first of the month, the next view to model update will set it + * to the first of the month. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="month-input-directive" module="monthExample"> + <file name="index.html"> + <script> + angular.module('monthExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 1); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a month int 2013: + <input id="exampleInput" type="month" name="input" ng-model="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> + <tt>value = {{value | date: "yyyy-MM"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), /** * @ngdoc input @@ -26031,6 +28397,8 @@ var inputType = { * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * + * The model must always be a number, otherwise Angular will throw an error. + * * @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`. @@ -26042,22 +28410,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="number-input-directive"> + <example name="number-input-directive" module="numberExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = 12; - } + angular.module('numberExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 12; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Number: <input type="number" name="input" ng-model="value" min="0" max="99" required> <span class="error" ng-show="myForm.input.$error.required"> @@ -26108,6 +28483,12 @@ var inputType = { * Text input with URL validation. Sets the `url` validation error key if the content is not a * valid URL. * + * <div class="alert alert-warning"> + * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex + * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify + * the built-in validators (see the {@link guide/forms Forms guide}) + * </div> + * * @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 Sets `required` validation error key if the value is not entered. @@ -26117,22 +28498,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="url-input-directive"> + <example name="url-input-directive" module="urlExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'http://google.com'; - } + angular.module('urlExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'http://google.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> URL: <input type="url" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -26184,6 +28572,12 @@ var inputType = { * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * + * <div class="alert alert-warning"> + * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex + * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can + * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) + * </div> + * * @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 Sets `required` validation error key if the value is not entered. @@ -26193,22 +28587,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="email-input-directive"> + <example name="email-input-directive" module="emailExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'me@example.com'; - } + angular.module('emailExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'me@example.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Email: <input type="email" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -26267,18 +28668,19 @@ var inputType = { * be set when selected. * * @example - <example name="radio-input-directive"> + <example name="radio-input-directive" module="radioExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.color = 'blue'; - $scope.specialValue = { - "id": "12345", - "value": "green" - }; - } + angular.module('radioExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.color = 'blue'; + $scope.specialValue = { + "id": "12345", + "value": "green" + }; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> <input type="radio" ng-model="color" value="red"> Red <br/> <input type="radio" ng-model="color" ng-value="specialValue"> Green <br/> <input type="radio" ng-model="color" value="blue"> Blue <br/> @@ -26311,24 +28713,25 @@ 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=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="checkbox-input-directive"> + <example name="checkbox-input-directive" module="checkboxExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value1 = true; - $scope.value2 = 'YES' - } + angular.module('checkboxExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value1 = true; + $scope.value2 = 'YES' + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Value1: <input type="checkbox" ng-model="value1"> <br/> Value2: <input type="checkbox" ng-model="value2" - ng-true-value="YES" ng-false-value="NO"> <br/> + ng-true-value="'YES'" ng-false-value="'NO'"> <br/> <tt>value1 = {{value1}}</tt><br/> <tt>value2 = {{value2}}</tt><br/> </form> @@ -26359,33 +28762,20 @@ var inputType = { 'file': noop }; -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. -function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; +function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } - -function addNativeHtml5Validators(ctrl, validatorName, element) { - var validity = element.prop('validity'); - if (isObject(validity)) { - var validator = function(value) { - // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can - // perform the required validation) - if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || - validity.typeMismatch) && !validity.valueMissing) { - ctrl.$setValidity(validatorName, false); - return; - } - return value; - }; - ctrl.$parsers.push(validator); - } +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); } -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var validity = element.prop('validity'); +function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var type = lowercase(element[0].type); + // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent @@ -26402,29 +28792,27 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - var listener = function() { + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } if (composing) return; - var value = element.val(); + var value = element.val(), + event = ev && ev.type; // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming - // e.g. <input ng-model="foo" ng-trim="false"> - if (toBoolean(attr.ngTrim || 'T')) { + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } - if (ctrl.$viewValue !== value || - // If the value is still empty/falsy, and there is no `required` error, run validators - // again. This enables HTML5 constraint validation errors to affect Angular validation - // even when the first character entered causes an error. - (validity && value === '' && !validity.valueMissing)) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } + // If a control is suffering from bad input (due to native validators), browsers discard its + // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the + // control's value is the same empty value twice in a row. + if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { + ctrl.$setViewValue(value, event); } }; @@ -26435,11 +28823,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } else { var timeout; - var deferListener = function() { + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { - listener(); timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } }); } }; @@ -26451,7 +28841,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(); + deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -26467,127 +28857,256 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; +} - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; +function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; + + if (existingDate) { + hours = existingDate.getHours(); + minutes = existingDate.getMinutes(); + seconds = existingDate.getSeconds(); + milliseconds = existingDate.getMilliseconds(); + } - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); + return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); + } + } + + return NaN; +} + +function createDateParser(regexp, mapping) { + return function(iso, date) { + var parts, map; + + if (isDate(iso)) { + return iso; + } + + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { + iso = iso.substring(1, iso.length - 1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); + + if (parts) { + parts.shift(); + if (date) { + map = { + yyyy: date.getFullYear(), + MM: date.getMonth() + 1, + dd: date.getDate(), + HH: date.getHours(), + mm: date.getMinutes(), + ss: date.getSeconds(), + sss: date.getMilliseconds() / 1000 + }; + } else { + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; } - return validateRegex(patternObj, value); - }; + + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + } } - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } + return NaN; + }; +} - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; +function createDateInputType(type, regexp, parseDate, format) { + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var previousDate; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + // Note: We cannot read ctrl.$modelValue, as there might be a different + // 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()); + } + return parsedDate; + } + return undefined; + }); - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); - } + ctrl.$formatters.push(function(value) { + if (value && !isDate(value)) { + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { + previousDate = value; + if (previousDate && timezone === 'UTC') { + var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); + previousDate = new Date(previousDate.getTime() + timezoneOffset); + } + return $filter('date')(value, format, timezone); + } else { + previousDate = null; + return ''; + } + }); - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function isValidDate(value) { + // Invalid Date: getTime() returns NaN + return value && !(value.getTime && value.getTime() !== value.getTime()); + } + + function parseObservedDateValue(val) { + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; + } + }; +} - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); +function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): + // - also sets validity.badInput (should only be validity.typeMismatch). + // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) + // - can ignore this case as we can still read out the erroneous email... + return validity.badInput && !validity.typeMismatch ? undefined : value; + }); } } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; - } + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; }); - addNativeHtml5Validators(ctrl, 'number', element); - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; }); - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); + if (attr.min || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); + attr.$observe('min', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + minVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); + if (attr.max || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); + attr.$observe('max', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { @@ -26596,13 +29115,13 @@ function radioInputType(scope, element, attr, ctrl) { element.attr('name', nextUid()); } - element.on('click', function() { + var listener = function(ev) { if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); + ctrl.$setViewValue(attr.value, ev && ev.type); } - }); + }; + + element.on('click', listener); ctrl.$render = function() { var value = attr.value; @@ -26612,30 +29131,42 @@ function radioInputType(scope, element, attr, ctrl) { attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; +} - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; +function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); + }; + + element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; - // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. ctrl.$isEmpty = function(value) { - return value !== trueValue; + return value === false; }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { @@ -26663,12 +29194,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * 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=} 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. */ @@ -26678,8 +29211,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @restrict E * * @description - * HTML input element control with angular data-binding. Input control follows HTML5 input types - * and polyfills the HTML5 validation behavior for older browsers. + * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding, + * input state control, and validation. + * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers. + * + * <div class="alert alert-warning"> + * **Note:** Not every feature offered is available for all input types. + * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`. + * </div> * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -26688,22 +29227,27 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * 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=} 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. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - <example name="input-directive"> + <example name="input-directive" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.user = {name: 'guest', last: 'visitor'}; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = {name: 'guest', last: 'visitor'}; + }]); </script> - <div ng-controller="Ctrl"> + <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"> @@ -26728,7 +29272,7 @@ function checkboxInputType(scope, element, attr, ctrl) { </div> </file> <file name="protractor.js" type="protractor"> - var user = element(by.binding('{{user}}')); + var user = element(by.exactBinding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); @@ -26782,14 +29326,17 @@ function checkboxInputType(scope, element, attr, ctrl) { </file> </example> */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', + function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + require: ['?ngModel'], + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); + } } } }; @@ -26798,55 +29345,117 @@ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + PENDING_CLASS = 'ng-pending'; /** * @ngdoc type * @name ngModel.NgModelController * * @property {string} $viewValue Actual string value in the view. - * @property {*} $modelValue The value in the model, that the control is bound to. + * @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. Each function is called, in turn, passing the value - through to the next. The last return value is used to populate the model. - Used to sanitize / convert the value as well as validation. For validation, - the parsers should update the validity state using - {@link ngModel.NgModelController#$setValidity $setValidity()}, - and return `undefined` for invalid values. + the control reads value from the DOM. The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. + +Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue +`$viewValue`}. + +Returning `undefined` from a parser means a parse error occurred. In that case, +no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` +will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} +is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. Each function is called, in turn, passing the value through to the - next. Used to format / convert values for display in the control and validation. - * ```js - * function formatter(value) { - * if (value) { - * return value.toUpperCase(); - * } - * } - * ngModel.$formatters.push(formatter); - * ``` + the model value changes. The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + Used to format / convert values for display in the control. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` + * + * @property {Object.<string, function>} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. + * + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` + * + * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator + * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators + * will only run once all synchronous validators have passed. + * + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. + * + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` * * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the * view value has changed. It is called with no arguments, and its return value is ignored. * This can be used in place of additional $watches against the model value. * - * @property {Object} $error An object hash with all errors as keys. + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. * + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. * @property {boolean} $valid True if there is no error. * @property {boolean} $invalid True if at least one error on the control. + * @property {string} $name The name attribute of the control. * * @description * - * `NgModelController` provides API for the `ng-model` directive. The controller contains - * services for data-binding, validation, CSS updates, and value formatting and parsing. It - * purposefully does not contain any logic which deals with DOM rendering or listening to - * DOM events. Such DOM related logic should be provided by other directives which make use of - * `NgModelController` for data-binding. + * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. + * The controller contains services for data-binding, validation, CSS updates, and value formatting + * and parsing. It purposefully does not contain any logic which deals with DOM rendering or + * listening to DOM events. + * Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding to control elements. + * Angular provides this DOM logic for most {@link input `input`} elements. + * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example + * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. * - * ## Custom Control Example + * @example + * ### Custom Control Example * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) * collaborate together to achieve the desired result. @@ -26854,7 +29463,12 @@ var VALID_CLASS = 'ng-valid', * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element * contents be edited in place by the user. This will not work on older browsers. * - * <example name="NgModelController" module="customControl"> + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * <example name="NgModelController" module="customControl" deps="angular-sanitize.js"> <file name="style.css"> [contenteditable] { border: 1px solid black; @@ -26868,22 +29482,22 @@ var VALID_CLASS = 'ng-valid', </file> <file name="script.js"> - angular.module('customControl', []). - directive('contenteditable', function() { + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { return { restrict: 'A', // only activate on element attribute require: '?ngModel', // get a hold of NgModelController link: function(scope, element, attrs, ngModel) { - if(!ngModel) return; // do nothing if no ng-model + if (!ngModel) return; // do nothing if no ng-model // Specify how UI should be updated ngModel.$render = function() { - element.html(ngModel.$viewValue || ''); + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); }; // Listen for change events to enable binding element.on('blur keyup change', function() { - scope.$apply(read); + scope.$evalAsync(read); }); read(); // initialize @@ -26892,14 +29506,14 @@ var VALID_CLASS = 'ng-valid', var html = element.html(); // When we clear the content editable the browser leaves a <br> behind // If strip-br attribute is provided then we strip this out - if( attrs.stripBr && html == '<br>' ) { + if ( attrs.stripBr && html == '<br>' ) { html = ''; } ngModel.$setViewValue(html); } } }; - }); + }]); </file> <file name="index.html"> <form name="myForm"> @@ -26934,26 +29548,60 @@ var VALID_CLASS = 'ng-valid', * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; + this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. + this.$validators = {}; + this.$asyncValidators = {}; this.$parsers = []; this.$formatters = []; this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; this.$pristine = true; this.$dirty = false; this.$valid = true; this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here + this.$name = $interpolate($attr.name || '', false)($scope); + + + var parsedNgModel = $parse($attr.ngModel), + parsedNgModelAssign = parsedNgModel.assign, + ngModelGet = parsedNgModel, + ngModelSet = parsedNgModelAssign, + pendingDebounce = null, + ctrl = this; + + this.$$setOptions = function(options) { + ctrl.$options = options; + if (options && options.getterSetter) { + var invokeModelGetter = $parse($attr.ngModel + '()'), + invokeModelSetter = $parse($attr.ngModel + '($$$p)'); + + ngModelGet = function($scope) { + var modelValue = parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + ngModelSet = function($scope, newValue) { + if (isFunction(parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + } else { + parsedNgModelAssign($scope, ctrl.$modelValue); + } + }; + } else if (!parsedNgModel.assign) { + throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); + } + }; /** * @ngdoc method @@ -26962,6 +29610,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Called when the view needs to be updated. It is expected that the user of the ng-model * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$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. + * + * 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` + * 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. */ this.$render = noop; @@ -26970,96 +29630,427 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @name ngModel.NgModelController#$isEmpty * * @description - * This is called when we need to determine if the value of the input is empty. + * This is called when we need to determine if the value of an input is empty. * * For instance, the required directive does this to work out if the input has data or not. + * * 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 * default. The `checkboxInputType` directive does this because in its case a value of `false` * implies empty. * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is empty. + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". */ this.$isEmpty = function(value) { return isUndefined(value) || value === '' || value === null || value !== value; }; var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - - - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + currentValidationRunId = 0; /** * @ngdoc method * @name ngModel.NgModelController#$setValidity * * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). + * Change the validity state, and notify the form. * - * This method should be called by validators - i.e. the parser or formatter functions. + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ + addSetValidityMethod({ + ctrl: this, + $element: $element, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + }, + parentForm: parentForm, + $animate: $animate + }); + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the `ng-dirty` class and set the control to its pristine + * state (`ng-pristine` class). A model is considered to be pristine when the control + * has not been changed from when first compiled. */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; + this.$setPristine = function() { + ctrl.$dirty = false; + ctrl.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setDirty + * + * @description + * Sets the control to its dirty state. + * + * This method can be called to remove the `ng-pristine` class and set the control to its dirty + * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed + * from when first compiled. + */ + this.$setDirty = function() { + ctrl.$dirty = true; + ctrl.$pristine = false; + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); + parentForm.$setDirty(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setUntouched + * + * @description + * Sets the control to its untouched state. + * + * This method can be called to remove the `ng-touched` class and set the control to its + * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + this.$setUntouched = function() { + ctrl.$touched = false; + ctrl.$untouched = true; + $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched + * + * @description + * Sets the control to its touched state. + * + * This method can be called to remove the `ng-untouched` class and set the control to its + * touched state (`ng-touched` class). A model is considered to be touched when the user has + * first focused the control element and then shifted focus away from the control (blur event). + */ + this.$setTouched = function() { + ctrl.$touched = true; + ctrl.$untouched = false; + $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue + * + * @description + * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, + * 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`. + * + * In this case, you can 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. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * <example name="ng-model-cancel-update" module="cancel-update-example"> + * <file name="app.js"> + * 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) { + * if (e.keyCode == 27) { + * $scope.myValue = ''; + * } + * }; + * }]); + * </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> + * + * <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 }}" + * </form> + * </div> + * </file> + * </example> + */ + this.$rollbackViewValue = function() { + $timeout.cancel(pendingDebounce); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue; + ctrl.$render(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then + * asynchronous validators). + * If the validity changes to invalid, the model will be set to `undefined`, + * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. + * If the validity changes to valid, it will set the model to the last available valid + * modelValue, i.e. either the last parsed value or the last value set from the scope. + */ + this.$validate = function() { + // ignore $validate before model is initialized + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + return; + } + + var viewValue = ctrl.$$lastCommittedViewValue; + // Note: we use the $$rawModelValue as $modelValue might have been + // set to undefined during a view -> model update that found validation + // errors. We can't parse the view here, since that could change + // the model although neither viewValue nor the model on the scope changed + var modelValue = ctrl.$$rawModelValue; + + // Check if the there's a parse error, so we don't unset it accidentially + var parserName = ctrl.$$parserName || 'parse'; + var parserValid = ctrl.$error[parserName] ? false : undefined; + + var prevValid = ctrl.$valid; + var prevModelValue = ctrl.$modelValue; + + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + + ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { + // If there was no change in validity, don't update the model + // This prevents changing an invalid modelValue to undefined + if (!allowInvalid && prevValid !== allValid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; + }); + + }; + + this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { + currentValidationRunId++; + var localValidationRunId = currentValidationRunId; + + // check parser error + if (!processParseErrors(parseValid)) { + validationDone(false); + return; + } + if (!processSyncValidators()) { + validationDone(false); + return; } + processAsyncValidators(); - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); + function processParseErrors(parseValid) { + var errorKey = ctrl.$$parserName || 'parse'; + if (parseValid === undefined) { + setValidity(errorKey, null); + } else { + setValidity(errorKey, parseValid); + if (!parseValid) { + forEach(ctrl.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + } + return true; + } - parentForm.$setValidity(validationErrorKey, isValid, this); + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(ctrl.$validators, function(validator, name) { + var result = validator(modelValue, viewValue); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(ctrl.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw $ngModelMinErr("$asyncValidators", + "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function(error) { + allValid = false; + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(true); + } else { + $q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === currentValidationRunId) { + ctrl.$setValidity(name, isValid); + } + } + + function validationDone(allValid) { + if (localValidationRunId === currentValidationRunId) { + + doneCallback(allValid); + } + } }; /** * @ngdoc method - * @name ngModel.NgModelController#$setPristine + * @name ngModel.NgModelController#$commitViewValue * * @description - * Sets the control to its pristine state. + * Commit a pending update to the `$modelValue`. * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(pendingDebounce); + + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + return; + } + ctrl.$$lastCommittedViewValue = viewValue; + + // change to dirty + if (ctrl.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }; + + this.$$parseAndValidate = function() { + var viewValue = ctrl.$$lastCommittedViewValue; + var modelValue = viewValue; + var parserValid = isUndefined(modelValue) ? undefined : true; + + if (parserValid) { + for (var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } + } + } + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + // ctrl.$modelValue has not been touched yet... + ctrl.$modelValue = ngModelGet($scope); + } + var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + ctrl.$$rawModelValue = modelValue; + + if (allowInvalid) { + ctrl.$modelValue = modelValue; + writeToModelIfNeeded(); + } + + // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. + // This can happen if e.g. $setViewValue is called from inside a parser + ctrl.$$runValidators(parserValid, modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + if (!allowInvalid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }; + + this.$$writeModelToScope = function() { + ngModelSet($scope, ctrl.$modelValue); + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + $exceptionHandler(e); + } + }); }; /** @@ -27069,73 +30060,108 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Update the view value. * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. + * 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. + * + * 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. * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * When this method 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. * + * 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 calling this function does not trigger a `$digest`. * * @param {string} value Value from the view. + * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - parentForm.$setDirty(); + this.$setViewValue = function(value, trigger) { + ctrl.$viewValue = value; + if (!ctrl.$options || ctrl.$options.updateOnDefault) { + ctrl.$$debounceViewValueCommit(trigger); } + }; - forEach(this.$parsers, function(fn) { - value = fn(value); - }); + this.$$debounceViewValueCommit = function(trigger) { + var debounceDelay = 0, + options = ctrl.$options, + debounce; + + if (options && isDefined(options.debounce)) { + debounce = options.debounce; + if (isNumber(debounce)) { + debounceDelay = debounce; + } else if (isNumber(debounce[trigger])) { + debounceDelay = debounce[trigger]; + } else if (isNumber(debounce['default'])) { + debounceDelay = debounce['default']; + } + } - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } + $timeout.cancel(pendingDebounce); + if (debounceDelay) { + pendingDebounce = $timeout(function() { + ctrl.$commitViewValue(); + }, debounceDelay); + } else if ($rootScope.$$phase) { + ctrl.$commitViewValue(); + } else { + $scope.$apply(function() { + ctrl.$commitViewValue(); }); } }; // model -> value - var ctrl = this; - + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + var modelValue = ngModelGet($scope); // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + // TODO(perf): why not move this to the action fn? + if (modelValue !== ctrl.$modelValue) { + ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; var formatters = ctrl.$formatters, idx = formatters.length; - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); } - - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); + + ctrl.$$runValidators(undefined, modelValue, viewValue, noop); } } - return value; + return modelValue; }); }]; @@ -27145,6 +30171,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @name ngModel * * @element input + * @priority 1 * * @description * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a @@ -27156,8 +30183,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` * require. * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -27166,7 +30193,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * For best practices on using `ngModel`, see: * - * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes] + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) * * For basic examples, how to use `ngModel`, see: * @@ -27177,6 +30204,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link input[number] number} * - {@link input[email] email} * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[datetime-local] datetime-local} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * @@ -27184,10 +30216,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * The following CSS classes are added and removed on the associated input/select/textarea element * depending on the validity of the model. * - * - `ng-valid` is set if the model is valid. - * - `ng-invalid` is set if the model is invalid. - * - `ng-pristine` is set if the model is pristine. - * - `ng-dirty` is set if the model is dirty. + * - `ng-valid`: the model is valid + * - `ng-invalid`: the model is invalid + * - `ng-valid-[key]`: for each valid key added by `$setValidity` + * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` + * - `ng-pristine`: the control hasn't been interacted with yet + * - `ng-dirty`: the control has been interacted with + * - `ng-touched`: the control has been blurred + * - `ng-untouched`: the control hasn't been blurred + * - `ng-pending`: any `$asyncValidators` are unfulfilled * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -27216,12 +30253,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * </pre> * * @example - * <example deps="angular-animate.js" animations="true" fixBase="true"> + * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = '1'; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = '1'; + }]); </script> <style> .my-input { @@ -27236,30 +30274,116 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </style> Update input to see transitions when valid/invalid. Integer is a valid value. - <form name="testForm" ng-controller="Ctrl"> + <form name="testForm" ng-controller="ExampleController"> <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" /> </form> </file> * </example> + * + * ## Binding to a getter/setter + * + * 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 + * to the view. + * + * <div class="alert alert-success"> + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + * </div> + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `<form>`, which will enable this behavior for all `<input>`s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * <example name="ngModel-getter-setter" module="getterSetterExample"> + <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 }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + if (angular.isDefined(newName)) { + _name = newName; + } + return _name; + } + }; + }]); + </file> + * </example> */ -var ngModelDirective = function() { +var ngModelDirective = ['$rootScope', function($rootScope) { return { - require: ['ngModel', '^?form'], + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; - formCtrl.$addControl(modelCtrl); + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); + + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + formCtrl.$$renameControl(modelCtrl, newValue); + } + }); + + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + element.on('blur', function(ev) { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(modelCtrl.$setTouched); + } else { + scope.$apply(modelCtrl.$setTouched); + } + }); + } + }; } }; -}; +}]; /** @@ -27271,7 +30395,15 @@ var ngModelDirective = function() { * The expression is evaluated immediately, unlike the JavaScript onchange event * which only triggers at the end of a change (usually, when the user leaves the * form element or presses the return key). - * The expression is not evaluated when the value change is coming from the model. + * + * The `ngChange` expression is only evaluated when a change in the input value causes + * a new value to be committed to the model. + * + * It will not be evaluated: + * * if the value returned from the `$parsers` transformation pipeline has not changed + * * if the input has continued to be invalid since the model will stay `null` + * * if the model is changed programmatically and not by a change to the input value + * * * Note, this directive requires `ngModel` to be present. * @@ -27280,17 +30412,18 @@ var ngModelDirective = function() { * in input value. * * @example - * <example name="ngChange-directive"> + * <example name="ngChange-directive" module="changeExample"> * <file name="index.html"> * <script> - * function Controller($scope) { - * $scope.counter = 0; - * $scope.change = function() { - * $scope.counter++; - * }; - * } + * angular.module('changeExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.counter = 0; + * $scope.change = function() { + * $scope.counter++; + * }; + * }]); * </script> - * <div ng-controller="Controller"> + * <div ng-controller="ExampleController"> * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> * <label for="ng-change-example2">Confirmed</label><br /> @@ -27321,6 +30454,7 @@ var ngModelDirective = function() { * </example> */ var ngChangeDirective = valueFn({ + restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ctrl) { ctrl.$viewChangeListeners.push(function() { @@ -27332,27 +30466,90 @@ var ngChangeDirective = valueFn({ var requiredDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; - } else { - ctrl.$setValidity('required', true); - return value; + ctrl.$validators.required = function(modelValue, viewValue) { + return !attr.required || !ctrl.$isEmpty(viewValue); + }; + + attr.$observe('required', function() { + ctrl.$validate(); + }); + } + }; +}; + + +var patternDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var regexp, patternExp = attr.ngPattern || attr.pattern; + attr.$observe('pattern', function(regex) { + if (isString(regex) && regex.length > 0) { + regex = new RegExp('^' + regex + '$'); + } + + if (regex && !regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(elm)); } + + regexp = regex || undefined; + ctrl.$validate(); + }); + + ctrl.$validators.pattern = function(value) { + return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; + } + }; +}; - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - attr.$observe('required', function() { - validator(ctrl.$viewValue); +var maxlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var maxlength = -1; + attr.$observe('maxlength', function(value) { + var intVal = int(value); + maxlength = isNaN(intVal) ? -1 : intVal; + ctrl.$validate(); }); + ctrl.$validators.maxlength = function(modelValue, viewValue) { + return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (viewValue.length <= maxlength); + }; + } + }; +}; + +var minlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var minlength = 0; + attr.$observe('minlength', function(value) { + minlength = int(value) || 0; + ctrl.$validate(); + }); + ctrl.$validators.minlength = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; + }; } }; }; @@ -27363,61 +30560,94 @@ var requiredDirective = function() { * @name ngList * * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. + * + * ### Example with Validation + * + * <example name="ngList-directive" module="listExample"> + * <file name="app.js"> + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * </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"> + * Required!</span> + * <br> + * <tt>names = {{names}}</tt><br/> + * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> + * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> + * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + * </form> + * </file> + * <file name="protractor.js" type="protractor"> + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); * - * @example - <example name="ngList-directive"> - <file name="index.html"> - <script> - function Ctrl($scope) { - $scope.names = ['igor', 'misko', 'vojta']; - } - </script> - <form name="myForm" ng-controller="Ctrl"> - List: <input name="namesInput" ng-model="names" ng-list required> - <span class="error" ng-show="myForm.namesInput.$error.required"> - Required!</span> - <br> - <tt>names = {{names}}</tt><br/> - <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> - <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> - </form> - </file> - <file name="protractor.js" type="protractor"> - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); - - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); - - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); - </file> - </example> + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * </file> + * </example> + * + * ### Example - splitting on whitespace + * <example name="ngList-directive-newlines"> + * <file name="index.html"> + * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> + * <pre>{{ list | json }}</pre> + * </file> + * <file name="protractor.js" type="protractor"> + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * </file> + * </example> + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { + restrict: 'A', + priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$attr.ngList) || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; var parse = function(viewValue) { // If the viewValue is invalid (say required but empty) it will be `undefined` @@ -27427,7 +30657,7 @@ var ngListDirective = function() { if (viewValue) { forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); + if (value) list.push(trimValues ? trim(value) : value); }); } @@ -27437,7 +30667,7 @@ var ngListDirective = function() { ctrl.$parsers.push(parse); ctrl.$formatters.push(function(value) { if (isArray(value)) { - return value.join(', '); + return value.join(ngList); } return undefined; @@ -27458,27 +30688,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `input[select]` or `input[radio]`, so - * that when the element is selected, the `ngModel` of that element is set to the - * bound value. + * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`}, + * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to + * the bound value. * - * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as - * shown below. + * `ngValue` is useful when dynamically generating lists of radio buttons using + * {@link ngRepeat `ngRepeat`}, as shown below. + * + * Likewise, `ngValue` can be used to generate `<option>` elements for + * the {@link select `select`} element. In that case however, only strings are supported + * for the `value `attribute, so the resulting `ngModel` will always be a string. + * Support for `select` models with non-string values is available via `ngOptions`. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute * of the `input` element * * @example - <example name="ngValue-directive"> + <example name="ngValue-directive" module="valueExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.names = ['pizza', 'unicorns', 'robots']; - $scope.my = { favorite: 'unicorns' }; - } + angular.module('valueExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.names = ['pizza', 'unicorns', 'robots']; + $scope.my = { favorite: 'unicorns' }; + }]); </script> - <form ng-controller="Ctrl"> + <form ng-controller="ExampleController"> <h2>Which is your favorite?</h2> <label ng-repeat="name in names" for="{{name}}"> {{name}} @@ -27506,6 +30742,7 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; */ var ngValueDirective = function() { return { + restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { @@ -27525,6 +30762,281 @@ var ngValueDirective = function() { /** * @ngdoc directive + * @name ngModelOptions + * + * @description + * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of + * events that will trigger a model update and/or a debouncing delay so that the actual update only + * 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 + * 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. + * + * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. + * + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * `ngModelOptions` has an effect on the element it's declared on and its descendants. + * + * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * - `updateOn`: string specifying which event should the input be bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging of the control. + * - `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} }"` + * - `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. + * + * @example + + The following example shows how to override immediate updates. Changes on the inputs within the + form will update the model only when the control loses focus (blur event). If `escape` key is + pressed while the input field is focused, the value is reset to the value in the current model. + + <example name="ngModelOptions-directive-blur" module="optionsExample"> + <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 /> + </form> + <pre>user.name = <span ng-bind="user.name"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say', data: '' }; + + $scope.cancel = function(e) { + if (e.keyCode == 27) { + $scope.userForm.userName.$rollbackViewValue(); + } + }; + }]); + </file> + <file name="protractor.js" type="protractor"> + var model = element(by.binding('user.name')); + var input = element(by.model('user.name')); + var other = element(by.model('user.data')); + + it('should allow custom events', function() { + input.sendKeys(' hello'); + input.click(); + expect(model.getText()).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say hello'); + }); + + it('should $rollbackViewValue when model changes', function() { + input.sendKeys(' hello'); + expect(input.getAttribute('value')).toEqual('say hello'); + input.sendKeys(protractor.Key.ESCAPE); + expect(input.getAttribute('value')).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say'); + }); + </file> + </example> + + This one shows how to debounce model changes. Model will be updated only 1 sec after last change. + If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + + <example name="ngModelOptions-directive-debounce" module="optionsExample"> + <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 /> + </form> + <pre>user.name = <span ng-bind="user.name"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say' }; + }]); + </file> + </example> + + This one shows how to bind to getter/setters: + + <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> + <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 }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + return angular.isDefined(newName) ? (_name = newName) : _name; + } + }; + }]); + </file> + </example> + */ +var ngModelOptionsDirective = function() { + return { + restrict: 'A', + controller: ['$scope', '$attrs', function($scope, $attrs) { + var that = this; + this.$options = $scope.$eval($attrs.ngModelOptions); + // Allow adding/overriding bound events + if (this.$options.updateOn !== undefined) { + 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() { + that.$options.updateOnDefault = true; + return ' '; + })); + } else { + this.$options.updateOnDefault = true; + } + }] + }; +}; + +// helper methods +function addSetValidityMethod(context) { + var ctrl = context.ctrl, + $element = context.$element, + classCache = {}, + set = context.set, + unset = context.unset, + parentForm = context.parentForm, + $animate = context.$animate; + + classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); + + ctrl.$setValidity = setValidity; + + function setValidity(validationErrorKey, state, options) { + if (state === undefined) { + createAndSet('$pending', validationErrorKey, options); + } else { + unsetAndCleanup('$pending', validationErrorKey, options); + } + if (!isBoolean(state)) { + unset(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } else { + if (state) { + unset(ctrl.$error, validationErrorKey, options); + set(ctrl.$$success, validationErrorKey, options); + } else { + set(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } + } + if (ctrl.$pending) { + cachedToggleClass(PENDING_CLASS, true); + ctrl.$valid = ctrl.$invalid = undefined; + toggleValidationCss('', null); + } else { + cachedToggleClass(PENDING_CLASS, false); + ctrl.$valid = isObjectEmpty(ctrl.$error); + ctrl.$invalid = !ctrl.$valid; + toggleValidationCss('', ctrl.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in ctrl.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (ctrl.$error[validationErrorKey]) { + combinedState = false; + } else if (ctrl.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + toggleValidationCss(validationErrorKey, combinedState); + parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + } + + function createAndSet(name, value, options) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, options); + } + + function unsetAndCleanup(name, value, options) { + if (ctrl[name]) { + unset(ctrl[name], value, options); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(className, switchValue) { + if (switchValue && !classCache[className]) { + $animate.addClass($element, className); + classCache[className] = true; + } else if (!switchValue && classCache[className]) { + $animate.removeClass($element, className); + classCache[className] = false; + } + } + + function toggleValidationCss(validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + return false; + } + } + return true; +} + +/** + * @ngdoc directive * @name ngBind * @restrict AC * @@ -27536,7 +31048,7 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * @@ -27549,14 +31061,15 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.name = 'Whirled'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.name = 'Whirled'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter name: <input type="text" ng-model="name"><br> Hello <span ng-bind="name"></span>! </div> @@ -27573,15 +31086,21 @@ var ngValueDirective = function() { </file> </example> */ -var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); -}); +var ngBindDirective = ['$compile', function($compile) { + return { + restrict: 'AC', + compile: function ngBindCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBind); + element = element[0]; + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + element.textContent = value === undefined ? '' : value; + }); + }; + } + }; +}]; /** @@ -27602,15 +31121,16 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { * * @example * Try it here: enter text in text box and watch the greeting change. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.salutation = 'Hello'; - $scope.name = 'World'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.salutation = 'Hello'; + $scope.name = 'World'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Salutation: <input type="text" ng-model="salutation"><br> Name: <input type="text" ng-model="name"><br> <pre ng-bind-template="{{salutation}} {{name}}!"></pre> @@ -27634,14 +31154,19 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { </file> </example> */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { + return { + compile: function ngBindTemplateCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindTemplateLink(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + element = element[0]; + attr.$observe('ngBindTemplate', function(value) { + element.textContent = value === undefined ? '' : value; + }); + }; + } }; }]; @@ -27651,13 +31176,15 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @name ngBindHtml * * @description - * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link - * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` - * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to + * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default, + * the resulting HTML content will be sanitized using the {@link ngSanitize.$sanitize $sanitize} service. + * To utilize this functionality, ensure that `$sanitize` is available, for example, by including {@link + * ngSanitize} in your module's dependencies (not in core Angular). In order to use {@link ngSanitize} + * in your module's dependencies, you need to include "angular-sanitize.js" in your application. + * + * You may also bypass sanitization for values you know are safe. To do so, bind to * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example - * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * under {@link ng.$sce#show-me-an-example-using-sce- Strict Contextual Escaping (SCE)}. * * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you * will have an exception (instead of an exploit.) @@ -27666,22 +31193,21 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. * * @example - Try it here: enter text in text box and watch the greeting change. - <example module="ngBindHtmlExample" deps="angular-sanitize.js"> + <example module="bindHtmlExample" deps="angular-sanitize.js"> <file name="index.html"> - <div ng-controller="ngBindHtmlCtrl"> + <div ng-controller="ExampleController"> <p ng-bind-html="myHTML"></p> </div> </file> <file name="script.js"> - angular.module('ngBindHtmlExample', ['ngSanitize']) - - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an <code>HTML</code>string with <a href="#">links!</a> and other <em>stuff</em>'; - }]); + angular.module('bindHtmlExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.myHTML = + 'I am an <code>HTML</code>string with ' + + '<a href="#">links!</a> and other <em>stuff</em>'; + }]); </file> <file name="protractor.js" type="protractor"> @@ -27692,16 +31218,26 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { </file> </example> */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); +var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { + return { + restrict: 'A', + compile: function ngBindHtmlCompile(tElement, tAttrs) { + var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { + return (value || '').toString(); + }); + $compile.$$addBindingClass(tElement); - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } + return function ngBindHtmlLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBindHtml); - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); + scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { + // we re-evaluate the expr because we want a TrustedValueHolderType + // for $sce, not a string + element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + }); + }; + } }; }]; @@ -27724,7 +31260,7 @@ function classDirective(name, selector) { scope.$watch('$index', function($index, old$index) { // jshint bitwise: false var mod = $index & 1; - if (mod !== old$index & 1) { + if (mod !== (old$index & 1)) { var classes = arrayClasses(scope.$eval(attr[name])); mod === selector ? addClasses(classes) : @@ -27743,10 +31279,10 @@ function classDirective(name, selector) { attr.$removeClass(newClasses); } - function digestClassCounts (classes, count) { + function digestClassCounts(classes, count) { var classCounts = element.data('$classCounts') || {}; var classesToUpdate = []; - forEach(classes, function (className) { + forEach(classes, function(className) { if (count > 0 || classCounts[className]) { classCounts[className] = (classCounts[className] || 0) + count; if (classCounts[className] === +(count > 0)) { @@ -27758,18 +31294,16 @@ function classDirective(name, selector) { return classesToUpdate.join(' '); } - function updateClasses (oldClasses, newClasses) { + function updateClasses(oldClasses, newClasses) { var toAdd = arrayDifference(newClasses, oldClasses); var toRemove = arrayDifference(oldClasses, newClasses); - toRemove = digestClassCounts(toRemove, -1); toAdd = digestClassCounts(toAdd, 1); - - if (toAdd.length === 0) { - $animate.removeClass(element, toRemove); - } else if (toRemove.length === 0) { + toRemove = digestClassCounts(toRemove, -1); + if (toAdd && toAdd.length) { $animate.addClass(element, toAdd); - } else { - $animate.setClass(element, toAdd, toRemove); + } + if (toRemove && toRemove.length) { + $animate.removeClass(element, toRemove); } } @@ -27783,7 +31317,7 @@ function classDirective(name, selector) { updateClasses(oldClasses, newClasses); } } - oldVal = copy(newVal); + oldVal = shallowCopy(newVal); } } }; @@ -27792,26 +31326,26 @@ function classDirective(name, selector) { var values = []; outer: - for(var i = 0; i < tokens1.length; i++) { + for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; + for (var j = 0; j < tokens2.length; j++) { + if (token == tokens2[j]) continue outer; } values.push(token); } return values; } - function arrayClasses (classVal) { + function arrayClasses(classVal) { if (isArray(classVal)) { return classVal; } else if (isString(classVal)) { return classVal.split(' '); } else if (isObject(classVal)) { - var classes = [], i = 0; + var classes = []; forEach(classVal, function(v, k) { if (v) { - classes.push(k); + classes = classes.concat(k.split(' ')); } }); return classes; @@ -27962,8 +31496,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 ngAnimate.$animate#addclass $animate.addClass} and - {@link ngAnimate.$animate#removeclass $animate.removeClass}. + to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and + {@link ng.$animate#removeClass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); @@ -28136,7 +31670,7 @@ var ngCloakDirective = ngDirective({ * * MVC components in angular: * - * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties + * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties * are accessed through bindings. * * View — The template (HTML with data bindings) that is rendered into the View. * * Controller — The `ngController` directive specifies a Controller class; the class contains business @@ -28149,177 +31683,212 @@ var ngCloakDirective = ngDirective({ * * @element ANY * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. + * @priority 500 + * @param {expression} ngController Name of a constructor function registered with the current + * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression} + * that on the current scope evaluates to a constructor function. + * + * The controller instance can be published into a scope property by specifying + * `ng-controller="as propertyName"`. + * + * If the current `$controllerProvider` is configured to use globals (via + * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may + * also be the name of a globally accessible constructor function (not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is shown in two different declaration styles you may use - * according to preference. - <example> - <file name="index.html"> - <script> - function SettingsController1() { - this.name = "John Smith"; - this.contacts = [ - {type: 'phone', value: '408 555 1212'}, - {type: 'email', value: 'john.smith@example.org'} ]; - }; - - SettingsController1.prototype.greet = function() { - alert(this.name); - }; - - SettingsController1.prototype.addContact = function() { - this.contacts.push({type: 'email', value: 'yourname@example.org'}); - }; - - SettingsController1.prototype.removeContact = function(contactToRemove) { - var index = this.contacts.indexOf(contactToRemove); - this.contacts.splice(index, 1); - }; - - SettingsController1.prototype.clearContact = function(contact) { - contact.type = 'phone'; - contact.value = ''; - }; - </script> - <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/> - Contact: - <ul> - <li ng-repeat="contact in settings.contacts"> - <select ng-model="contact.type"> - <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> ] - </li> - <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> - </ul> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should check controller as', function() { - var container = element(by.id('ctrl-as-exmpl')); - - expect(container.findElement(by.model('settings.name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click(); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - </file> - </example> - <example> - <file name="index.html"> - <script> - function SettingsController2($scope) { - $scope.name = "John Smith"; - $scope.contacts = [ - {type:'phone', value:'408 555 1212'}, - {type:'email', value:'john.smith@example.org'} ]; - - $scope.greet = function() { - alert(this.name); - }; - - $scope.addContact = function() { - this.contacts.push({type:'email', value:'yourname@example.org'}); - }; - - $scope.removeContact = function(contactToRemove) { - var index = this.contacts.indexOf(contactToRemove); - this.contacts.splice(index, 1); - }; - - $scope.clearContact = function(contact) { - contact.type = 'phone'; - contact.value = ''; - }; - } - </script> - <div id="ctrl-exmpl" ng-controller="SettingsController2"> - Name: <input type="text" ng-model="name"/> - [ <a href="" ng-click="greet()">greet</a> ]<br/> - Contact: - <ul> - <li ng-repeat="contact in contacts"> - <select ng-model="contact.type"> - <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> ] - </li> - <li>[ <a href="" ng-click="addContact()">add</a> ]</li> - </ul> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should check controller', function() { - var container = element(by.id('ctrl-exmpl')); - - expect(container.findElement(by.model('name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click(); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - </file> - </example> + * easily be called from the angular markup. Any changes to the data are automatically reflected + * in the View without the need for a manual update. + * + * Two different declaration styles are included below: + * + * * one binds methods and properties directly onto the controller using `this`: + * `ng-controller="SettingsController1 as settings"` + * * one injects `$scope` into the controller: + * `ng-controller="SettingsController2"` + * + * The second option is more common in the Angular community, and is generally used in boilerplates + * and in this guide. However, there are advantages to binding properties directly to the controller + * and avoiding scope. + * + * * Using `controller as` makes it obvious which controller you are accessing in the template when + * multiple controllers apply to an element. + * * If you are writing your controllers as classes you have easier access to the properties and + * methods, which will appear on the scope, from inside the controller code. + * * Since there is always a `.` in the bindings, you don't have to worry about prototypal + * inheritance masking primitives. + * + * This example demonstrates the `controller as` syntax. + * + * <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/> + * Contact: + * <ul> + * <li ng-repeat="contact in settings.contacts"> + * <select ng-model="contact.type"> + * <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> ] + * </li> + * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> + * </ul> + * </div> + * </file> + * <file name="app.js"> + * angular.module('controllerAsExample', []) + * .controller('SettingsController1', SettingsController1); + * + * function SettingsController1() { + * this.name = "John Smith"; + * this.contacts = [ + * {type: 'phone', value: '408 555 1212'}, + * {type: 'email', value: 'john.smith@example.org'} ]; + * } + * + * SettingsController1.prototype.greet = function() { + * alert(this.name); + * }; + * + * SettingsController1.prototype.addContact = function() { + * this.contacts.push({type: 'email', value: 'yourname@example.org'}); + * }; + * + * SettingsController1.prototype.removeContact = function(contactToRemove) { + * var index = this.contacts.indexOf(contactToRemove); + * this.contacts.splice(index, 1); + * }; + * + * SettingsController1.prototype.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * </file> + * <file name="protractor.js" type="protractor"> + * it('should check controller as', function() { + * var container = element(by.id('ctrl-as-exmpl')); + * expect(container.element(by.model('settings.name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in settings.contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in settings.contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.linkText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.linkText('add')).click(); + * + * expect(container.element(by.repeater('contact in settings.contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * </file> + * </example> + * + * This example demonstrates the "attach to `$scope`" style of controller. + * + * <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/> + * Contact: + * <ul> + * <li ng-repeat="contact in contacts"> + * <select ng-model="contact.type"> + * <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> ] + * </li> + * <li>[ <a href="" ng-click="addContact()">add</a> ]</li> + * </ul> + * </div> + * </file> + * <file name="app.js"> + * angular.module('controllerExample', []) + * .controller('SettingsController2', ['$scope', SettingsController2]); + * + * function SettingsController2($scope) { + * $scope.name = "John Smith"; + * $scope.contacts = [ + * {type:'phone', value:'408 555 1212'}, + * {type:'email', value:'john.smith@example.org'} ]; + * + * $scope.greet = function() { + * alert($scope.name); + * }; + * + * $scope.addContact = function() { + * $scope.contacts.push({type:'email', value:'yourname@example.org'}); + * }; + * + * $scope.removeContact = function(contactToRemove) { + * var index = $scope.contacts.indexOf(contactToRemove); + * $scope.contacts.splice(index, 1); + * }; + * + * $scope.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * } + * </file> + * <file name="protractor.js" type="protractor"> + * it('should check controller', function() { + * var container = element(by.id('ctrl-exmpl')); + * + * expect(container.element(by.model('name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.linkText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.linkText('add')).click(); + * + * expect(container.element(by.repeater('contact in contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * </file> + *</example> */ var ngControllerDirective = [function() { return { + restrict: 'A', scope: true, controller: '@', priority: 500 @@ -28334,11 +31903,13 @@ var ngControllerDirective = [function() { * @description * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. * - * This is necessary when developing things like Google Chrome Extensions. + * 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 us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * 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 * * 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 @@ -28349,7 +31920,18 @@ var ngControllerDirective = [function() { * 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. * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * 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: + * + * ``` + * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of + * script in the following Content Security Policy directive: "default-src 'self'". Note that + * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. + * ``` + * + * 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. * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * @@ -28362,11 +31944,129 @@ var ngControllerDirective = [function() { ... </html> ``` - */ + * @example + // Note: the suffix `.csp` in the example name triggers + // csp mode in our http server! + <example name="example.csp" module="cspExample" ng-csp="true"> + <file name="index.html"> + <div ng-controller="MainController as ctrl"> + <div> + <button ng-click="ctrl.inc()" id="inc">Increment</button> + <span id="counter"> + {{ctrl.counter}} + </span> + </div> + + <div> + <button ng-click="ctrl.evil()" id="evil">Evil</button> + <span id="evilError"> + {{ctrl.evilError}} + </span> + </div> + </div> + </file> + <file name="script.js"> + angular.module('cspExample', []) + .controller('MainController', function() { + this.counter = 0; + this.inc = function() { + this.counter++; + }; + this.evil = function() { + // jshint evil:true + try { + eval('1+2'); + } catch (e) { + this.evilError = e.message; + } + }; + }); + </file> + <file name="protractor.js" type="protractor"> + var util, webdriver; -// 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 a csp() fn that looks for ng-csp attribute -// anywhere in the current doc + var incBtn = element(by.id('inc')); + var counter = element(by.id('counter')); + var evilBtn = element(by.id('evil')); + var evilError = element(by.id('evilError')); + + function getAndClearSevereErrors() { + return browser.manage().logs().get('browser').then(function(browserLog) { + return browserLog.filter(function(logEntry) { + return logEntry.level.value > webdriver.logging.Level.WARNING.value; + }); + }); + } + + function clearErrors() { + getAndClearSevereErrors(); + } + + function expectNoErrors() { + getAndClearSevereErrors().then(function(filteredLog) { + expect(filteredLog.length).toEqual(0); + if (filteredLog.length) { + console.log('browser console errors: ' + util.inspect(filteredLog)); + } + }); + } + + function expectError(regex) { + getAndClearSevereErrors().then(function(filteredLog) { + var found = false; + filteredLog.forEach(function(log) { + if (log.message.match(regex)) { + found = true; + } + }); + if (!found) { + throw new Error('expected an error that matches ' + regex); + } + }); + } + + beforeEach(function() { + util = require('util'); + webdriver = require('protractor/node_modules/selenium-webdriver'); + }); + + // For now, we only test on Chrome, + // as Safari does not load the page with Protractor's injected scripts, + // and Firefox webdriver always disables content security policy (#6358) + if (browser.params.browser !== 'chrome') { + return; + } + + it('should not report errors when the page is loaded', function() { + // clear errors so we are not dependent on previous tests + clearErrors(); + // Need to reload the page as the page is already loaded when + // we come here + browser.driver.getCurrentUrl().then(function(url) { + browser.get(url); + }); + expectNoErrors(); + }); + + it('should evaluate expressions', function() { + expect(counter.getText()).toEqual('0'); + incBtn.click(); + expect(counter.getText()).toEqual('1'); + expectNoErrors(); + }); + + it('should throw and report an error when using "eval"', function() { + evilBtn.click(); + expect(evilError.getText()).toMatch(/Content Security Policy/); + expectError(/Content Security Policy/); + }); + </file> + </example> + */ + +// 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 /** * @ngdoc directive @@ -28387,7 +32087,9 @@ var ngControllerDirective = [function() { <button ng-click="count = count + 1" ng-init="count=0"> Increment </button> - count: {{count}} + <span> + count: {{count}} + </span> </file> <file name="protractor.js" type="protractor"> it('should check ng-click', function() { @@ -28399,25 +32101,41 @@ var ngControllerDirective = [function() { </example> */ /* - * A directive that allows creation of custom onclick handlers that are defined as angular - * expressions and are compiled and executed within the current scope. - * - * Events that are handled via these handler are always configured not to propagate further. + * A collection of directives that allows creation of custom event handlers that are defined as + * angular expressions and are compiled and executed within the current scope. */ var ngEventDirectives = {}; + +// For events that might fire synchronously during DOM manipulation +// we need to execute their event handlers asynchronously using $evalAsync, +// so that they are not executed in an inconsistent state. +var forceAsyncEvents = { + 'blur': true, + 'focus': true +}; forEach( 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), - function(name) { - var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { + function(eventName) { + var directiveName = directiveNormalize('ng-' + eventName); + ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) { return { + restrict: 'A', compile: function($element, attr) { - var fn = $parse(attr[directiveName]); - return function(scope, element, attr) { - element.on(lowercase(name), function(event) { - scope.$apply(function() { + // We expose the powerful $event object on the scope that provides access to the Window, + // etc. that isn't protected by the fast paths in $parse. We explicitly request better + // checks at the cost of speed since event handler expressions are not executed as + // frequently as regular change detection. + var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); + return function ngEventHandler(scope, element) { + element.on(eventName, function(event) { + var callback = function() { fn(scope, {$event:event}); - }); + }; + if (forceAsyncEvents[eventName] && $rootScope.$$phase) { + scope.$evalAsync(callback); + } else { + scope.$apply(callback); + } }); }; } @@ -28630,8 +32348,13 @@ forEach( * @example <example> <file name="index.html"> - <input ng-keyup="count = count + 1" ng-init="count=0"> - key up count: {{count}} + <p>Typing in the input box below updates the key count</p> + <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}} + + <p>Typing in the input box below updates the keycode</p> + <input ng-keyup="event=$event"> + <p>event keyCode: {{ event.keyCode }}</p> + <p>event altKey: {{ event.altKey }}</p> </file> </example> */ @@ -28670,27 +32393,35 @@ forEach( * server and reloading the current page), but only if the form does not contain `action`, * `data-action`, or `x-action` attributes. * + * <div class="alert alert-warning"> + * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and + * `ngSubmit` handlers together. See the + * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} + * for a detailed discussion of when `ngSubmit` may be triggered. + * </div> + * * @element form * @priority 0 * @param {expression} ngSubmit {@link guide/expression Expression} to eval. * ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example module="submitExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = []; - $scope.text = 'hello'; - $scope.submit = function() { - if ($scope.text) { - $scope.list.push(this.text); - $scope.text = ''; - } - }; - } + angular.module('submitExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = []; + $scope.text = 'hello'; + $scope.submit = function() { + if ($scope.text) { + $scope.list.push(this.text); + $scope.text = ''; + } + }; + }]); </script> - <form ng-submit="submit()" ng-controller="Ctrl"> + <form ng-submit="submit()" ng-controller="ExampleController"> Enter text and hit enter: <input type="text" ng-model="text" name="text" /> <input type="submit" id="submit" value="Submit" /> @@ -28702,7 +32433,7 @@ forEach( expect(element(by.binding('list')).getText()).toBe('list=[]'); element(by.css('#submit')).click(); expect(element(by.binding('list')).getText()).toContain('hello'); - expect(element(by.input('text')).getAttribute('value')).toBe(''); + expect(element(by.model('text')).getAttribute('value')).toBe(''); }); it('should ignore empty strings', function() { expect(element(by.binding('list')).getText()).toBe('list=[]'); @@ -28721,6 +32452,10 @@ forEach( * @description * Specify custom behavior on focus event. * + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon @@ -28737,6 +32472,14 @@ forEach( * @description * Specify custom behavior on blur event. * + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. + * + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon @@ -28828,7 +32571,7 @@ forEach( * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope * is created when the element is restored. The scope created within `ngIf` inherits from * its parent scope using - * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance). + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). * An important implication of this is if `ngModel` is used within `ngIf` to bind to * a javascript primitive defined in the parent scope. In this case any modifications made to the * variable within the child scope will override (hide) the value in the parent scope. @@ -28842,8 +32585,8 @@ forEach( * and `leave` effects. * * @animations - * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container - * leave - happens just before the ngIf contents are removed from the DOM + * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container + * leave - happens just before the `ngIf` contents are removed from the DOM * * @element ANY * @scope @@ -28858,7 +32601,7 @@ forEach( Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> Show when checked: <span ng-if="checked" class="animate-if"> - I'm removed when the checkbox is unchecked. + This is removed when the checkbox is unchecked. </span> </file> <file name="animations.css"> @@ -28887,23 +32630,24 @@ forEach( */ var ngIfDirective = ['$animate', function($animate) { return { + multiElement: true, transclude: 'element', priority: 600, terminal: true, restrict: 'A', $$tlb: true, - link: function ($scope, $element, $attr, ctrl, $transclude) { + link: function($scope, $element, $attr, ctrl, $transclude) { var block, childScope, previousElements; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - if (toBoolean(value)) { + if (value) { if (!childScope) { - childScope = $scope.$new(); - $transclude(childScope, function (clone) { + $transclude(function(clone, newScope) { + childScope = newScope; clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); // 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 - // by a directive with templateUrl when it's template arrives. + // by a directive with templateUrl when its template arrives. block = { clone: clone }; @@ -28911,17 +32655,17 @@ var ngIfDirective = ['$animate', function($animate) { }); } } else { - if(previousElements) { + if (previousElements) { previousElements.remove(); previousElements = null; } - if(childScope) { + if (childScope) { childScope.$destroy(); childScope = null; } - if(block) { - previousElements = getBlockElements(block.clone); - $animate.leave(previousElements, function() { + if (block) { + previousElements = getBlockNodes(block.clone); + $animate.leave(previousElements).then(function() { previousElements = null; }); block = null; @@ -28941,10 +32685,10 @@ var ngIfDirective = ['$animate', function($animate) { * Fetches, compiles and includes an external HTML fragment. * * By default, the template URL is restricted to the same domain and protocol as the - * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * application document. This is done by calling {@link $sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or - * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link + * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link * ng.$sce Strict Contextual Escaping}. * * In addition, the browser's @@ -28975,9 +32719,9 @@ var ngIfDirective = ['$animate', function($animate) { * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="includeExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="template" ng-options="t.name for t in templates"> <option value="">(blank)</option> </select> @@ -28989,12 +32733,13 @@ var ngIfDirective = ['$animate', function($animate) { </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'}, - { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; + $scope.template = $scope.templates[0]; + }]); </file> <file name="template1.html"> Content of template1.html @@ -29057,7 +32802,7 @@ var ngIfDirective = ['$animate', function($animate) { return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(2).click(); + templateSelect.all(by.css('option')).get(2).click(); expect(includeElem.getText()).toMatch(/Content of template2.html/); }); @@ -29067,7 +32812,7 @@ var ngIfDirective = ['$animate', function($animate) { return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(0).click(); + templateSelect.all(by.css('option')).get(0).click(); expect(includeElem.isPresent()).toBe(false); }); </file> @@ -29081,6 +32826,9 @@ var ngIfDirective = ['$animate', function($animate) { * @eventType emit on the scope ngInclude was declared in * @description * Emitted every time the ngInclude content is requested. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ @@ -29090,9 +32838,24 @@ var ngIfDirective = ['$animate', function($animate) { * @eventType emit on the current ngInclude scope * @description * Emitted every time the ngInclude content is reloaded. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $animate, $sce) { + + +/** + * @ngdoc event + * @name ngInclude#$includeContentError + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ +var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce', + function($templateRequest, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', priority: 400, @@ -29111,16 +32874,16 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' currentElement; var cleanupLastIncludeContent = function() { - if(previousElement) { + if (previousElement) { previousElement.remove(); previousElement = null; } - if(currentScope) { + if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement, function() { + if (currentElement) { + $animate.leave(currentElement).then(function() { previousElement = null; }); previousElement = currentElement; @@ -29137,7 +32900,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' var thisChangeId = ++changeCounter; if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { + //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 (thisChangeId !== changeCounter) return; var newScope = scope.$new(); ctrl.template = response; @@ -29150,18 +32915,21 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element, afterAnimation); + $animate.enter(clone, null, $element).then(afterAnimation); }); currentScope = newScope; currentElement = clone; - currentScope.$emit('$includeContentLoaded'); + currentScope.$emit('$includeContentLoaded', src); scope.$eval(onloadExp); - }).error(function() { - if (thisChangeId === changeCounter) cleanupLastIncludeContent(); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } }); - scope.$emit('$includeContentRequested'); + scope.$emit('$includeContentRequested', src); } else { cleanupLastIncludeContent(); ctrl.template = null; @@ -29184,6 +32952,18 @@ var ngIncludeFillContentDirective = ['$compile', priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { + if (/SVG/.test($element[0].toString())) { + // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not + // support innerHTML, so detect this here and try to generate the contents + // specially. + $element.empty(); + $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope, + function namespaceAdaptedClone(clone) { + $element.append(clone); + }, {futureParentElement: $element}); + return; + } + $element.html(ctrl.template); $compile($element.contents())(scope); } @@ -29219,14 +32999,15 @@ var ngIncludeFillContentDirective = ['$compile', * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - <example> + <example module="initExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = [['a', 'b'], ['c', 'd']]; - } + angular.module('initExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = [['a', 'b'], ['c', 'd']]; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <div ng-repeat="innerList in list" ng-init="outerIndex = $index"> <div ng-repeat="value in innerList" ng-init="innerIndex = $index"> <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span> @@ -29366,7 +33147,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * When one person, perhaps John, views the document, "John is viewing" will be shown. * When three people view the document, no explicit number rule is found, so * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. - * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing" * is shown. * * Note that when you specify offsets, you must provide explicit number rules for @@ -29379,16 +33160,17 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * @param {number=} offset Offset to deduct from the total number. * * @example - <example> + <example module="pluralizeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.person1 = 'Igor'; - $scope.person2 = 'Misko'; - $scope.personCount = 1; - } + angular.module('pluralizeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.person1 = 'Igor'; + $scope.person2 = 'Misko'; + $scope.personCount = 1; + }]); </script> - <div ng-controller="Ctrl"> + <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/> @@ -29462,7 +33244,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); </example> */ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { - var BRACE = /{}/g; + var BRACE = /{}/g, + IS_WHEN = /^when(Minus)?(.+)$/; + return { restrict: 'EA', link: function(scope, element, attr) { @@ -29473,34 +33257,44 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp whensExpFns = {}, startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - isWhen = /^when(Minus)?(.+)$/; + braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol, + watchRemover = angular.noop, + lastCount; forEach(attr, function(expression, attributeName) { - if (isWhen.test(attributeName)) { - whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = - element.attr(attr.$attr[attributeName]); + var tmpMatch = IS_WHEN.exec(attributeName); + if (tmpMatch) { + var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]); + whens[whenKey] = element.attr(attr.$attr[attributeName]); } }); forEach(whens, function(expression, key) { - whensExpFns[key] = - $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + - offset + endSymbol)); + whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement)); + }); - scope.$watch(function ngPluralizeWatch() { - var value = parseFloat(scope.$eval(numberExp)); + scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) { + var count = parseFloat(newVal); + var countIsNaN = isNaN(count); - if (!isNaN(value)) { - //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, - //check it against pluralization rules in $locale service - if (!(value in whens)) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); - } else { - return ''; + if (!countIsNaN && !(count in whens)) { + // If an explicit number rule such as 1, 2, 3... is defined, just use it. + // Otherwise, check it against pluralization rules in $locale service. + count = $locale.pluralCat(count - offset); + } + + // 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))) { + watchRemover(); + watchRemover = scope.$watch(whensExpFns[count], updateElementText); + lastCount = count; } - }, function ngPluralizeWatchAction(newVal) { - element.text(newVal); }); + + function updateElementText(newText) { + element.text(newText || ''); + } } }; }]; @@ -29602,7 +33396,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, * before specifying a tracking expression. * - * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements + * 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. * * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique @@ -29617,6 +33411,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter * to items in conjunction with a tracking expression. * + * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the + * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message + * when a filter is active on the repeater, but the filtered result set is empty. + * + * 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. + * * @example * This example initializes the scope to a list of names and * then uses `ngRepeat` to display every person: @@ -29637,9 +33438,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." /> <ul class="example-animate-container"> - <li class="animate-repeat" ng-repeat="friend in friends | filter:q"> + <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> + <li class="animate-repeat" ng-if="results.length == 0"> + <strong>No results found...</strong> + </li> </ul> </div> </file> @@ -29706,29 +33510,84 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); + + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + // TODO(perf): generate setters to shave off ~40ms or 1-1.5% + scope[valueIdentifier] = value; + if (keyIdentifier) scope[keyIdentifier] = key; + scope.$index = index; + scope.$first = (index === 0); + scope.$last = (index === (arrayLength - 1)); + scope.$middle = !(scope.$first || scope.$last); + // jshint bitwise: false + scope.$odd = !(scope.$even = (index&1) === 0); + // jshint bitwise: true + }; + + var getBlockStart = function(block) { + return block.clone[0]; + }; + + var getBlockEnd = function(block) { + return block.clone[block.clone.length - 1]; + }; + + return { + restrict: 'A', + multiElement: true, transclude: 'element', priority: 1000, terminal: true, $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, - lhs, rhs, valueIdentifier, keyIdentifier, - hashFnLocals = {$id: hashKey}; - - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + compile: function ngRepeatCompile($element, $attr) { + var expression = $attr.ngRepeat; + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); - } + } + + var lhs = match[1]; + var rhs = match[2]; + var aliasAs = match[3]; + var trackByExp = match[4]; - lhs = match[1]; - rhs = match[2]; - trackByExp = match[3]; + match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/); + + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + var valueIdentifier = match[3] || match[1]; + var keyIdentifier = match[2]; - if (trackByExp) { - trackByExpGetter = $parse(trackByExp); + if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || + /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(aliasAs))) { + throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.", + aliasAs); + } + + var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; + var hashFnLocals = {$id: hashKey}; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + } else { + trackByIdArrayFn = function(key, value) { + return hashKey(value); + }; + trackByIdObjFn = function(key) { + return key; + }; + } + + return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { + + if (trackByExpGetter) { trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions if (keyIdentifier) hashFnLocals[keyIdentifier] = key; @@ -29736,48 +33595,39 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { hashFnLocals.$index = index; return trackByExpGetter($scope, hashFnLocals); }; - } else { - trackByIdArrayFn = function(key, value) { - return hashKey(value); - }; - trackByIdObjFn = function(key) { - return key; - }; - } - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); } - valueIdentifier = match[3] || match[1]; - keyIdentifier = match[2]; // Store a list of elements from previous run. This is a hash where key is the item from the // iterator, and the value is objects with following properties. // - scope: bound scope // - element: previous element. // - index: position - var lastBlockMap = {}; + // + // We are using no-proto object so that we don't need to guard against inherited props via + // hasOwnProperty. + var lastBlockMap = createMap(); //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + $scope.$watchCollection(rhs, function ngRepeatAction(collection) { var index, length, - previousNode = $element[0], // current position of the node + previousNode = $element[0], // node that cloned nodes should be inserted after + // initialized to the comment node anchor nextNode, // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. - nextBlockMap = {}, - arrayLength, - childScope, + nextBlockMap = createMap(), + collectionLength, key, value, // key/value of iteration trackById, trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = [], + nextBlockOrder, elementsToRemove; + if (aliasAs) { + $scope[aliasAs] = collection; + } if (isArrayLike(collection)) { collectionKeys = collection; @@ -29786,128 +33636,118 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { trackByIdFn = trackByIdExpFn || trackByIdObjFn; // if object, extract keys, sort them and use to determine order of iteration over obj props collectionKeys = []; - for (key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - collectionKeys.push(key); + for (var itemKey in collection) { + if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') { + collectionKeys.push(itemKey); } } collectionKeys.sort(); } - arrayLength = collectionKeys.length; + collectionLength = collectionKeys.length; + nextBlockOrder = new Array(collectionLength); // locate existing items - length = nextBlockOrder.length = collectionKeys.length; - for(index = 0; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if(lastBlockMap.hasOwnProperty(trackById)) { - block = lastBlockMap[trackById]; - delete lastBlockMap[trackById]; - nextBlockMap[trackById] = block; - nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap - forEach(nextBlockOrder, function(block) { - if (block && block.scope) lastBlockMap[block.id] = block; - }); - // This is a duplicate and we need to throw an error - throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); - } else { - // new never before seen block - nextBlockOrder[index] = { id: trackById }; - nextBlockMap[trackById] = false; - } - } + for (index = 0; index < collectionLength; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + if (lastBlockMap[trackById]) { + // found previously seen block + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap[trackById]) { + // if collision detected. restore lastBlockMap and throw an error + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + throw ngRepeatMinErr('dupes', + "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", + expression, trackById, value); + } else { + // new never before seen block + nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; + nextBlockMap[trackById] = true; + } + } - // remove existing items - for (key in lastBlockMap) { - // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn - if (lastBlockMap.hasOwnProperty(key)) { - block = lastBlockMap[key]; - elementsToRemove = getBlockElements(block.clone); - $animate.leave(elementsToRemove); - forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); - block.scope.$destroy(); + // remove leftover items + for (var blockKey in lastBlockMap) { + block = lastBlockMap[blockKey]; + elementsToRemove = getBlockNodes(block.clone); + $animate.leave(elementsToRemove); + if (elementsToRemove[0].parentNode) { + // if the element was not removed yet because of pending animation, mark it as deleted + // so that we can ignore it later + for (index = 0, length = elementsToRemove.length; index < length; index++) { + elementsToRemove[index][NG_REMOVED] = true; + } } + block.scope.$destroy(); } // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = collectionKeys.length; index < length; index++) { + for (index = 0; index < collectionLength; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; - if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { // if we have already seen this object, then we need to reuse the // associated scope/element - childScope = block.scope; nextNode = previousNode; + + // skip nodes that are already pending removal via leave animation do { nextNode = nextNode.nextSibling; - } while(nextNode && nextNode[NG_REMOVED]); + } while (nextNode && nextNode[NG_REMOVED]); if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); } else { // new item which we don't know about - childScope = $scope.$new(); - } + $transclude(function ngRepeatTransclude(clone, scope) { + block.scope = scope; + // http://jsperf.com/clone-vs-createcomment + var endNode = ngRepeatEndComment.cloneNode(false); + clone[clone.length++] = endNode; - childScope[valueIdentifier] = value; - if (keyIdentifier) childScope[keyIdentifier] = key; - childScope.$index = index; - childScope.$first = (index === 0); - childScope.$last = (index === (arrayLength - 1)); - childScope.$middle = !(childScope.$first || childScope.$last); - // jshint bitwise: false - childScope.$odd = !(childScope.$even = (index&1) === 0); - // jshint bitwise: true - - if (!block.scope) { - $transclude(childScope, function(clone) { - clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? $animate.enter(clone, null, jqLite(previousNode)); - previousNode = clone; - block.scope = childScope; + 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 - // by a directive with templateUrl when it's template arrives. + // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); }); } } lastBlockMap = nextBlockMap; }); + }; } }; - - function getBlockStart(block) { - return block.clone[0]; - } - - function getBlockEnd(block) { - return block.clone[block.clone.length - 1]; - } }]; +var NG_HIDE_CLASS = 'ng-hide'; +var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; /** * @ngdoc directive * @name ngShow * * @description * The `ngShow` directive shows or hides the given HTML element based on the expression - * provided to the ngShow attribute. The element is shown or hidden by removing or adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding + * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined * in AngularJS and sets the display style to none (using an !important flag). * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * @@ -29919,13 +33759,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When true, the ng-hide CSS class is removed + * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed * from the element causing the element not to appear hidden. * * ## Why is !important used? * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. * This also becomes a bigger issue when dealing with CSS frameworks. @@ -29934,30 +33774,25 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. * - * ### Overriding .ng-hide + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: * ```css * .ng-hide { - * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default... - * display:block!important; - * - * //this is just another form of hiding an element - * position:absolute; - * top:-9999px; - * left:-9999px; + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; * } * ``` * - * Just remember to include the important flag so the CSS override will function. + * By default you don't need to override in CSS anything and the animations will work around the display style. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * - * ## A note about animations with ngShow + * ## A note about animations with `ngShow` * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression * is true and false. This system works like the animation system present with ngClass except that @@ -29969,8 +33804,15 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * //a working example can be found at the bottom of this page * // * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition:0.5s linear all; - * display:block!important; + * /* this is required as of 1.3x to properly + * apply all styling in a show/hide animation */ + * transition: 0s linear all; + * } + * + * .my-element.ng-hide-add-active, + * .my-element.ng-hide-remove-active { + * /* the transition is defined in the active class */ + * transition: 1s linear all; * } * * .my-element.ng-hide-add { ... } @@ -29979,9 +33821,12 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * * @animations - * addClass: .ng-hide - happens after the ngShow expression evaluates to a truthy value and the just before contents are set to visible - * removeClass: .ng-hide - happens after the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden * * @element ANY * @param {expression} ngShow If the {@link guide/expression expression} is truthy @@ -30005,34 +33850,33 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </div> </file> <file name="glyphicons.css"> - @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> .animate-show { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; } - .animate-show.ng-hide-add, - .animate-show.ng-hide-remove { - display:block!important; + .animate-show.ng-hide-add.ng-hide-add-active, + .animate-show.ng-hide-remove.ng-hide-remove-active { + -webkit-transition: all linear 0.5s; + transition: all linear 0.5s; } .animate-show.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; + line-height: 0; + opacity: 0; + padding: 0 10px; } .check-element { - padding:10px; - border:1px solid black; - background:white; + padding: 10px; + border: 1px solid black; + background: white; } </file> <file name="protractor.js" type="protractor"> @@ -30052,10 +33896,20 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </example> */ var ngShowDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value) { + // we're adding a temporary, animation-specific class for ng-hide since this way + // we can control when the element is actually displayed on screen without having + // to have a global/greedy CSS selector that breaks when other animations are run. + // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } }; }]; @@ -30066,26 +33920,26 @@ var ngShowDirective = ['$animate', function($animate) { * * @description * The `ngHide` directive shows or hides the given HTML element based on the expression - * provided to the ngHide attribute. The element is shown or hidden by removing or adding + * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined * in AngularJS and sets the display style to none (using an !important flag). * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * * ```html * <!-- when $scope.myValue is truthy (element is hidden) --> - * <div ng-hide="myValue"></div> + * <div ng-hide="myValue" class="ng-hide"></div> * * <!-- when $scope.myValue is falsy (element is visible) --> - * <div ng-hide="myValue" class="ng-hide"></div> + * <div ng-hide="myValue"></div> * ``` * - * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When false, the ng-hide CSS class is removed + * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed * from the element causing the element not to appear hidden. * * ## Why is !important used? * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. * This also becomes a bigger issue when dealing with CSS frameworks. @@ -30094,43 +33948,36 @@ var ngShowDirective = ['$animate', function($animate) { * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. * - * ### Overriding .ng-hide + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: * ```css * .ng-hide { - * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default... - * display:block!important; - * - * //this is just another form of hiding an element - * position:absolute; - * top:-9999px; - * left:-9999px; + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; * } * ``` * - * Just remember to include the important flag so the CSS override will function. + * By default you don't need to override in CSS anything and the animations will work around the display style. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * - * ## A note about animations with ngHide + * ## A note about animations with `ngHide` * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass, except that - * you must also include the !important flag to override the display property so - * that you can perform an animation when the element is hidden during the time of the animation. + * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` + * CSS class is added and removed for you instead of your own CSS class. * * ```css * // * //a working example can be found at the bottom of this page * // * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition:0.5s linear all; - * display:block!important; + * transition: 0.5s linear all; * } * * .my-element.ng-hide-add { ... } @@ -30139,9 +33986,12 @@ var ngShowDirective = ['$animate', function($animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * * @animations - * removeClass: .ng-hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden - * addClass: .ng-hide - happens after the ngHide expression evaluates to a non truthy value and just before the contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible * * @element ANY * @param {expression} ngHide If the {@link guide/expression expression} is truthy then @@ -30165,34 +34015,29 @@ var ngShowDirective = ['$animate', function($animate) { </div> </file> <file name="glyphicons.css"> - @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> .animate-hide { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; - } - - .animate-hide.ng-hide-add, - .animate-hide.ng-hide-remove { - display:block!important; + -webkit-transition: all linear 0.5s; + transition: all linear 0.5s; + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; } .animate-hide.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; + line-height: 0; + opacity: 0; + padding: 0 10px; } .check-element { - padding:10px; - border:1px solid black; - background:white; + padding: 10px; + border: 1px solid black; + background: white; } </file> <file name="protractor.js" type="protractor"> @@ -30212,10 +34057,18 @@ var ngShowDirective = ['$animate', function($animate) { </example> */ var ngHideDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value) { + // The comment inside of the ngShowDirective explains why we add and + // remove a temporary class for the show/hide animation + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } }; }]; @@ -30228,14 +34081,20 @@ var ngHideDirective = ['$animate', function($animate) { * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. * * @element ANY - * @param {expression} ngStyle {@link guide/expression Expression} which evals to an - * object whose keys are CSS style names and values are corresponding values for those CSS - * keys. + * @param {expression} ngStyle + * + * {@link guide/expression Expression} which evals to an + * object whose keys are CSS style names and values are corresponding values for those CSS + * keys. + * + * Since some CSS style names are not valid keys for an object, they must be quoted. + * See the 'background-color' style in the example below. * * @example <example> <file name="index.html"> - <input type="button" value="set" ng-click="myStyle={color:'red'}"> + <input type="button" value="set color" ng-click="myStyle={color:'red'}"> + <input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}"> <input type="button" value="clear" ng-click="myStyle={}"> <br/> <span ng-style="myStyle">Sample Text</span> @@ -30251,7 +34110,7 @@ var ngHideDirective = ['$animate', function($animate) { it('should check ng-style', function() { expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); - element(by.css('input[value=set]')).click(); + element(by.css('input[value=\'set color\']')).click(); expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); element(by.css('input[value=clear]')).click(); expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); @@ -30299,15 +34158,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM * * @usage + * + * ``` * <ANY ng-switch="expression"> * <ANY ng-switch-when="matchValue1">...</ANY> * <ANY ng-switch-when="matchValue2">...</ANY> * <ANY ng-switch-default>...</ANY> * </ANY> + * ``` * * * @scope - * @priority 800 + * @priority 1200 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. * On child elements add: * @@ -30320,9 +34182,9 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="switchExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="selection" ng-options="item for item in items"> </select> <tt>selection={{selection}}</tt> @@ -30336,10 +34198,11 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.items = ['settings', 'home', 'other']; - $scope.selection = $scope.items[0]; - } + angular.module('switchExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + }]); </file> <file name="animations.css"> .animate-switch-container { @@ -30382,11 +34245,11 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { expect(switchElem.getText()).toMatch(/Settings Div/); }); it('should change to home', function() { - select.element.all(by.css('option')).get(1).click(); + select.all(by.css('option')).get(1).click(); expect(switchElem.getText()).toMatch(/Home Span/); }); it('should select default', function() { - select.element.all(by.css('option')).get(2).click(); + select.all(by.css('option')).get(2).click(); expect(switchElem.getText()).toMatch(/default/); }); </file> @@ -30403,47 +34266,41 @@ var ngSwitchDirective = ['$animate', function($animate) { }], link: function(scope, element, attr, ngSwitchController) { var watchExpr = attr.ngSwitch || attr.on, - selectedTranscludes, - selectedElements, - previousElements, + selectedTranscludes = [], + selectedElements = [], + previousLeaveAnimations = [], selectedScopes = []; + var spliceFactory = function(array, index) { + return function() { array.splice(index, 1); }; + }; + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - var i, ii = selectedScopes.length; - if(ii > 0) { - if(previousElements) { - for (i = 0; i < ii; i++) { - previousElements[i].remove(); - } - previousElements = null; - } + var i, ii; + for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { + $animate.cancel(previousLeaveAnimations[i]); + } + previousLeaveAnimations.length = 0; - previousElements = []; - for (i= 0; i<ii; i++) { - var selected = selectedElements[i]; - selectedScopes[i].$destroy(); - previousElements[i] = selected; - $animate.leave(selected, function() { - previousElements.splice(i, 1); - if(previousElements.length === 0) { - previousElements = null; - } - }); - } + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = getBlockNodes(selectedElements[i].clone); + selectedScopes[i].$destroy(); + var promise = previousLeaveAnimations[i] = $animate.leave(selected); + promise.then(spliceFactory(previousLeaveAnimations, i)); } - selectedElements = []; - selectedScopes = []; + selectedElements.length = 0; + selectedScopes.length = 0; if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { - scope.$eval(attr.change); forEach(selectedTranscludes, function(selectedTransclude) { - var selectedScope = scope.$new(); - selectedScopes.push(selectedScope); - selectedTransclude.transclude(selectedScope, function(caseElement) { + selectedTransclude.transclude(function(caseElement, selectedScope) { + selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; + caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + var block = { clone: caseElement }; - selectedElements.push(caseElement); + selectedElements.push(block); $animate.enter(caseElement, anchor.parent(), anchor); }); }); @@ -30455,8 +34312,9 @@ var ngSwitchDirective = ['$animate', function($animate) { var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attrs, ctrl, $transclude) { ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); @@ -30465,8 +34323,9 @@ var ngSwitchWhenDirective = ngDirective({ var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attr, ctrl, $transclude) { ctrl.cases['?'] = (ctrl.cases['?'] || []); ctrl.cases['?'].push({ transclude: $transclude, element: element }); @@ -30476,7 +34335,7 @@ var ngSwitchDefaultDirective = ngDirective({ /** * @ngdoc directive * @name ngTransclude - * @restrict AC + * @restrict EAC * * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. @@ -30486,15 +34345,10 @@ var ngSwitchDefaultDirective = ngDirective({ * @element ANY * * @example - <example module="transclude"> + <example module="transcludeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.title = 'Lorem Ipsum'; - $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - } - - angular.module('transclude', []) + angular.module('transcludeExample', []) .directive('pane', function(){ return { restrict: 'E', @@ -30502,13 +34356,17 @@ var ngSwitchDefaultDirective = ngDirective({ scope: { title:'@' }, template: '<div style="border: 1px solid black;">' + '<div style="background-color: gray">{{title}}</div>' + - '<div ng-transclude></div>' + + '<ng-transclude></ng-transclude>' + '</div>' }; - }); + }) + .controller('ExampleController', ['$scope', function($scope) { + $scope.title = 'Lorem Ipsum'; + $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + }]); </script> - <div ng-controller="Ctrl"> - <input ng-model="title"><br> + <div ng-controller="ExampleController"> + <input ng-model="title"> <br/> <textarea ng-model="text"></textarea> <br/> <pane title="{{title}}">{{text}}</pane> </div> @@ -30529,6 +34387,7 @@ var ngSwitchDefaultDirective = ngDirective({ * */ var ngTranscludeDirective = ngDirective({ + restrict: 'EAC', link: function($scope, $element, $attrs, controller, $transclude) { if (!$transclude) { throw minErr('ngTransclude')('orphan', @@ -30585,7 +34444,6 @@ var scriptDirective = ['$templateCache', function($templateCache) { compile: function(element, attr) { if (attr.type == 'text/ng-template') { var templateUrl = attr.id, - // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent text = element[0].text; $templateCache.put(templateUrl, text); @@ -30609,26 +34467,49 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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, the `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 `select as`. `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. * - * <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> - * * 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:** `ngOptions` provides an iterator facility for the `<option>` element which should be used instead - * of {@link ng.directive:ngRepeat ngRepeat} when you want the - * `select` model to be bound to a non-string value. This is because an option element can only - * be bound to string values at present. + * **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> * + * ## `select as` + * + * Using `select as` will bind the result of the `select as` 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` with `track by` + * + * Using `select as` together with `track by` is not recommended. Reasoning: + * + * - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> + * values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItem'}}], + * $scope.selected = {name: 'aSubItem'}; + * - track by is always applied to `value`, with the purpose of preserving the selection, + * (to `item` in this case) + * - to calculate whether an item is selected we do the following: + * 1. apply `track by` to the values in the array, e.g. + * 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}`. + * * @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. @@ -30640,8 +34521,10 @@ var ngOptionsMinErr = minErr('ngOptions'); * * for array data sources: * * `label` **`for`** `value` **`in`** `array` * * `select` **`as`** `label` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` + * * `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` @@ -30663,24 +34546,26 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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`). + * `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> + <example module="selectExample"> <file name="index.html"> <script> - function MyCntrl($scope) { - $scope.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - $scope.color = $scope.colors[2]; // red - } + 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="MyCntrl"> + <div ng-controller="ExampleController"> <ul> <li ng-repeat="color in colors"> Name: <input ng-model="color.name"> @@ -30692,43 +34577,47 @@ var ngOptionsMinErr = minErr('ngOptions'); </ul> <hr/> Color (null not allowed): - <select ng-model="color" ng-options="c.name for c in colors"></select><br> + <select ng-model="myColor" ng-options="color.name for color in colors"></select><br> Color (null allowed): <span class="nullable"> - <select ng-model="color" ng-options="c.name for c in colors"> + <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="color" ng-options="c.name group by c.shade for c in colors"> + <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors"> </select><br/> - Select <a href ng-click="color={name:'not in list'}">bogus</a>.<br> + Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br> <hr/> - Currently selected: {{ {selected_color:color} }} + Currently selected: {{ {selected_color:myColor} }} <div style="border:solid 1px black; height:20px" - ng-style="{'background-color':color.name}"> + 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:color}')).getText()).toMatch('red'); - element.all(by.select('color')).first().click(); - element.all(by.css('select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('black'); - element(by.css('.nullable select[ng-model="color"]')).click(); - element.all(by.css('.nullable select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('null'); + 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({ terminal: true }); +var ngOptionsDirective = valueFn({ + restrict: 'A', + terminal: true +}); + // jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 @@ -30757,7 +34646,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }; - self.addOption = function(value) { + self.addOption = function(value, element) { assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; @@ -30765,13 +34654,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { $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) { + if (ngModelCtrl.$viewValue === value) { this.renderUnknownOption(value); } } @@ -30807,6 +34702,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { 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')), @@ -30814,7 +34710,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { unknownOption = optionTemplate.clone(); // find "null" option - for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) { + for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) { if (children[i].value === '') { emptyOption = nullOption = children.eq(i); break; @@ -30877,7 +34773,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // we need to work of an array, so we need to see if anything was inserted/removed scope.$watch(function selectMultipleWatch() { if (!equals(lastView, ctrl.$viewValue)) { - lastView = copy(ctrl.$viewValue); + lastView = shallowCopy(ctrl.$viewValue); ctrl.$render(); } }); @@ -30908,17 +34804,22 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { 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:''}]]; + 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 @@ -30936,154 +34837,202 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // clear contents, we'll add what's needed based on the model selectElement.empty(); - selectElement.on('change', function() { - scope.$apply(function() { - var optionGroup, - collection = valuesFn(scope) || [], - locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; + 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) { - value = []; - for (groupIndex = 0, groupLength = optionGroupsCache.length; - groupIndex < groupLength; - groupIndex++) { - // list of options for that group. (first item has the parent) - optionGroup = optionGroupsCache[groupIndex]; - - for(index = 1, length = optionGroup.length; index < length; index++) { - if ((optionElement = optionGroup[index].element)[0].selected) { - key = optionElement.val(); - if (keyName) locals[keyName] = key; - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) break; - } - } else { - locals[valueName] = collection[key]; - } - value.push(valueFn(scope, locals)); - } - } - } + viewValue = []; + forEach(selectElement.val(), function(selectedKey) { + selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey; + viewValue.push(getViewValue(selectedKey, collection[selectedKey])); + }); } else { - key = selectElement.val(); - if (key == '?') { - value = undefined; - } else if (key === ''){ - value = null; - } else { - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) { - value = valueFn(scope, locals); - break; - } - } - } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); - } + 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]); } - // Update the null option's selected property here so $render cleans it up correctly - if (optionGroupsCache[0].length > 1) { - if (optionGroupsCache[0][1].id !== key) { - optionGroupsCache[0][1].selected = false; - } + } + } + 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); } - ctrl.$setViewValue(value); - }); - }); + } else if (trackFn) { + viewValue = callExpression(trackFn, null, viewValue); + } - ctrl.$render = render; + return function isSelected(key, value) { + var compareValueFn; + if (trackFn) { + compareValueFn = trackFn; + } else if (selectAsFn) { + compareValueFn = selectAsFn; + } else { + compareValueFn = valueFn; + } - // TODO(vojta): can't we optimize this ? - scope.$watch(render); + 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() { - // Temporary location for the option groups before we render them + renderScheduled = false; + + // Temporary location for the option groups before we render them var optionGroups = {'':[]}, optionGroupNames = [''], optionGroupName, optionGroup, option, existingParent, existingOptions, existingOption, - modelValue = ctrl.$modelValue, + viewValue = ctrl.$viewValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, key, + value, groupLength, length, groupIndex, index, - locals = {}, + labelMap = {}, selected, - selectedSet = false, // nothing is selected yet + isSelected = createIsSelectedFn(viewValue), + anySelected = false, lastElement, element, - label; + label, + optionId; - if (multiple) { - if (trackFn && isArray(modelValue)) { - selectedSet = new HashMap([]); - for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { - locals[valueName] = modelValue[trackIndex]; - selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); - } - } else { - selectedSet = new HashMap(modelValue); - } - } + 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; - locals[keyName] = key; + if (key.charAt(0) === '$') continue; } + value = values[key]; - locals[valueName] = values[key]; - - optionGroupName = groupByFn(scope, locals) || ''; + optionGroupName = callExpression(groupByFn, key, value) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } - if (multiple) { - selected = isDefined( - selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) - ); - } else { - if (trackFn) { - var modelCast = {}; - modelCast[valueName] = modelValue; - selected = trackFn(scope, modelCast) === trackFn(scope, locals); - } else { - selected = modelValue === valueFn(scope, locals); - } - selectedSet = selectedSet || selected; // see if at least one item is selected - } - label = displayFn(scope, locals); // what will be seen by the user + + 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: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), + id: optionId, label: label, selected: selected // determine if we should be selected }); } if (!multiple) { - if (nullOption || modelValue === null) { + if (nullOption || viewValue === null) { // insert null option if we have a placeholder, or the model is null - optionGroups[''].unshift({id:'', label:'', selected:!selectedSet}); - } else if (!selectedSet) { + 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}); } @@ -31119,20 +35068,29 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } lastElement = null; // start at the beginning - for(index = 0, length = optionGroup.length; index < length; index++) { + for (index = 0, length = optionGroup.length; index < length; index++) { option = optionGroup[index]; - if ((existingOption = existingOptions[index+1])) { + 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 (existingOption.selected !== option.selected) { + 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 @@ -31147,7 +35105,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // 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); } @@ -31157,6 +35117,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { id: option.id, selected: option.selected }); + updateLabelMap(labelMap, option.label, true); if (lastElement) { lastElement.after(element); } else { @@ -31167,14 +35128,28 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } // remove any excessive OPTIONs in a group index++; // increment since the existingOptions[0] is parent element not OPTION - while(existingOptions.length > index) { - existingOptions.pop().element.remove(); + 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) { - optionGroupsCache.pop()[0].element.remove(); + 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); + } + }); } } } @@ -31198,28 +35173,26 @@ var optionDirective = ['$interpolate', function($interpolate) { } } - return function (scope, element, attr) { + return function(scope, element, attr) { var selectCtrlName = '$selectController', parent = element.parent(), selectCtrl = parent.data(selectCtrlName) || parent.parent().data(selectCtrlName); // in case we are in optgroup - if (selectCtrl && selectCtrl.databound) { - // For some reason Opera defaults to true and if not overridden this messes up the repeater. - // We don't want the view to drive the initialization of the model anyway. - element.prop('selected', false); - } else { + if (!selectCtrl || !selectCtrl.databound) { selectCtrl = nullSelectCtrl; } if (interpolateFn) { scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { attr.$set('value', newVal); - if (newVal !== oldVal) selectCtrl.removeOption(oldVal); - selectCtrl.addOption(newVal); + if (oldVal !== newVal) { + selectCtrl.removeOption(oldVal); + } + selectCtrl.addOption(newVal, element); }); } else { - selectCtrl.addOption(attr.value); + selectCtrl.addOption(attr.value, element); } element.on('$destroy', function() { @@ -31232,7 +35205,7 @@ var optionDirective = ['$interpolate', function($interpolate) { var styleDirective = valueFn({ restrict: 'E', - terminal: true + terminal: false }); /** @@ -31346,7 +35319,7 @@ angular.scenario.setUpAndRun = function(config) { } angular.forEach(angular.scenario.output, function(fn, name) { - if (!output.length || indexOf(output,name) != -1) { + if (!output.length || output.indexOf(name) != -1) { var context = body.append('<div></div>').find('div:last'); context.attr('id', name); fn.call({}, context, $runner, objModel); @@ -31451,10 +35424,10 @@ function callerFile(offset) { if (line) { if (line.indexOf('@') !== -1) { // Firefox - line = line.substring(line.indexOf('@')+1); + line = line.substring(line.indexOf('@') + 1); } else { // Chrome - line = line.substring(line.indexOf('(')+1).replace(')', ''); + line = line.substring(line.indexOf('(') + 1).replace(')', ''); } } @@ -31472,7 +35445,7 @@ function callerFile(offset) { * * To work around this we instead use our own handler that fires a real event. */ -(function(fn){ +(function(fn) { // We need a handle to the original trigger function for input tests. var parentTrigger = fn._originalTrigger = fn.trigger; fn.trigger = function(type) { @@ -31502,7 +35475,7 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { bindSelector = '.ng-binding:visible'; if (angular.isString(bindExp)) { bindExp = bindExp.replace(/\s/g, ''); - match = function (actualExp) { + match = function(actualExp) { if (actualExp) { actualExp = actualExp.replace(/\s/g, ''); if (actualExp == bindExp) return true; @@ -31528,7 +35501,7 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { function push(value) { if (value === undefined) { value = ''; - } else if (typeof value != 'string') { + } else if (typeof value !== 'string') { value = angular.toJson(value); } result.push('' + value); @@ -31536,27 +35509,21 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { selection.each(function() { var element = windowJquery(this), - binding; - if (binding = element.data('$binding')) { - if (typeof binding == 'string') { - if (match(binding)) { - push(element.scope().$eval(binding)); - } - } else { - if (!angular.isArray(binding)) { - binding = [binding]; + bindings; + if (bindings = element.data('$binding')) { + for (var expressions = [], binding, j=0, jj=bindings.length; j < jj; j++) { + binding = bindings[j]; + + if (binding.expressions) { + expressions = binding.expressions; + } else { + expressions = [binding]; } - for(var fns, j=0, jj=binding.length; j<jj; j++) { - fns = binding[j]; - if (fns.parts) { - fns = fns.parts; - } else { - fns = [fns]; - } - for (var scope, fn, i = 0, ii = fns.length; i < ii; i++) { - if(match((fn = fns[i]).exp)) { - push(fn(scope = scope || element.scope())); - } + for (var scope, expression, i = 0, ii = expressions.length; i < ii; i++) { + expression = expressions[i]; + if (match(expression)) { + scope = scope || element.scope(); + push(scope.$eval(expression)); } } } @@ -31566,19 +35533,6 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { }; (function() { - var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10); - - function indexOf(array, obj) { - if (array.indexOf) return array.indexOf(obj); - - for ( var i = 0; i < array.length; i++) { - if (obj === array[i]) return i; - } - return -1; - } - - - /** * Triggers a browser event. Attempts to choose the right event if one is * not specified. @@ -31599,7 +35553,6 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { var inputType = (element.type) ? element.type.toLowerCase() : null, nodeName = element.nodeName.toLowerCase(); - if (!eventType) { eventType = { 'text': 'change', @@ -31626,100 +35579,74 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) { keys = keys || []; function pressed(key) { - return indexOf(keys, key) !== -1; + return keys.indexOf(key) !== -1; } - if (msie < 9) { - if (inputType == 'radio' || inputType == 'checkbox') { - element.checked = !element.checked; - } - - // WTF!!! Error: Unspecified error. - // Don't know why, but some elements when detached seem to be in inconsistent state and - // calling .fireEvent() on them will result in very unhelpful error (Error: Unspecified error) - // forcing the browser to compute the element position (by reading its CSS) - // puts the element in consistent state. - element.style.posLeft; - - // TODO(vojta): create event objects with pressed keys to get it working on IE<9 - var ret = element.fireEvent('on' + eventType); - if (inputType == 'submit') { - while(element) { - if (element.nodeName.toLowerCase() == 'form') { - element.fireEvent('onsubmit'); - break; - } - element = element.parentNode; - } + var evnt; + if (/transitionend/.test(eventType)) { + if (window.WebKitTransitionEvent) { + evnt = new WebKitTransitionEvent(eventType, eventData); + evnt.initEvent(eventType, false, true); } - return ret; - } else { - var evnt; - if(/transitionend/.test(eventType)) { - if(window.WebKitTransitionEvent) { - evnt = new WebKitTransitionEvent(eventType, eventData); - evnt.initEvent(eventType, false, true); + else { + try { + evnt = new TransitionEvent(eventType, eventData); } - else { - try { - evnt = new TransitionEvent(eventType, eventData); - } - catch(e) { - evnt = document.createEvent('TransitionEvent'); - evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); - } + catch (e) { + evnt = document.createEvent('TransitionEvent'); + evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0); } } - else if(/animationend/.test(eventType)) { - if(window.WebKitAnimationEvent) { - evnt = new WebKitAnimationEvent(eventType, eventData); - evnt.initEvent(eventType, false, true); + } + else if (/animationend/.test(eventType)) { + if (window.WebKitAnimationEvent) { + evnt = new WebKitAnimationEvent(eventType, eventData); + evnt.initEvent(eventType, false, true); + } + else { + try { + evnt = new AnimationEvent(eventType, eventData); } - else { - try { - evnt = new AnimationEvent(eventType, eventData); - } - catch(e) { - evnt = document.createEvent('AnimationEvent'); - evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0); - } + catch (e) { + evnt = document.createEvent('AnimationEvent'); + evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0); } } - else { - evnt = document.createEvent('MouseEvents'); - x = x || 0; - y = y || 0; - evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), - pressed('alt'), pressed('shift'), pressed('meta'), 0, element); - } - - /* we're unable to change the timeStamp value directly so this - * is only here to allow for testing where the timeStamp value is - * read */ - evnt.$manualTimeStamp = eventData.timeStamp; - - if(!evnt) return; - - var originalPreventDefault = evnt.preventDefault, - appWindow = element.ownerDocument.defaultView, - fakeProcessDefault = true, - finalProcessDefault, - angular = appWindow.angular || {}; - - // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 - angular['ff-684208-preventDefault'] = false; - evnt.preventDefault = function() { - fakeProcessDefault = false; - return originalPreventDefault.apply(evnt, arguments); - }; + } + else { + evnt = document.createEvent('MouseEvents'); + x = x || 0; + y = y || 0; + evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'), + pressed('alt'), pressed('shift'), pressed('meta'), 0, element); + } - element.dispatchEvent(evnt); - finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); + /* we're unable to change the timeStamp value directly so this + * is only here to allow for testing where the timeStamp value is + * read */ + evnt.$manualTimeStamp = eventData.timeStamp; - delete angular['ff-684208-preventDefault']; + if (!evnt) return; - return finalProcessDefault; - } + var originalPreventDefault = evnt.preventDefault, + appWindow = element.ownerDocument.defaultView, + fakeProcessDefault = true, + finalProcessDefault, + angular = appWindow.angular || {}; + + // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208 + angular['ff-684208-preventDefault'] = false; + evnt.preventDefault = function() { + fakeProcessDefault = false; + return originalPreventDefault.apply(evnt, arguments); + }; + + element.dispatchEvent(evnt); + finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault); + + delete angular['ff-684208-preventDefault']; + + return finalProcessDefault; }; }()); @@ -31836,7 +35763,7 @@ angular.scenario.Application.prototype.executeAction = function(action) { return $injector; }; - $injector.invoke(function($browser){ + $injector.invoke(function($browser) { $browser.notifyWhenNoOutstandingRequests(function() { action.call(self, $window, $element); }); @@ -31960,7 +35887,7 @@ angular.scenario.Describe.prototype.it = function(name, body) { */ angular.scenario.Describe.prototype.iit = function(name, body) { this.it.apply(this, arguments); - this.its[this.its.length-1].only = true; + this.its[this.its.length - 1].only = true; }; /** @@ -32026,7 +35953,7 @@ angular.scenario.Future.prototype.execute = function(doneFn) { if (result) { try { result = self.parser(result); - } catch(e) { + } catch (e) { error = e; } } @@ -32036,7 +35963,7 @@ angular.scenario.Future.prototype.execute = function(doneFn) { }; /** - * Configures the future to convert it's final with a function fn(value) + * Configures the future to convert its final with a function fn(value) * * @param {function()} fn function(value) that returns the parsed value */ @@ -32046,7 +35973,7 @@ angular.scenario.Future.prototype.parsedWith = function(fn) { }; /** - * Configures the future to parse it's final value from JSON + * Configures the future to parse its final value from JSON * into objects. */ angular.scenario.Future.prototype.fromJson = function() { @@ -32054,7 +35981,7 @@ angular.scenario.Future.prototype.fromJson = function() { }; /** - * Configures the future to convert it's final value from objects + * Configures the future to convert its final value from objects * into JSON. */ angular.scenario.Future.prototype.toJson = function() { @@ -32272,7 +36199,7 @@ angular.scenario.ObjectModel.Spec.prototype.addStep = function(name) { * @return {Object} the step */ angular.scenario.ObjectModel.Spec.prototype.getLastStep = function() { - return this.steps[this.steps.length-1]; + return this.steps[this.steps.length - 1]; }; /** @@ -32657,7 +36584,7 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, }); var result = $document.find(selector); if (selector.match(NG)) { - angular.forEach(['[ng-','[data-ng-','[x-ng-'], function(value, index){ + angular.forEach(['[ng-','[data-ng-','[x-ng-'], function(value, index) { result = result.add(selector.replace(NG, value), $document); }); } @@ -32673,7 +36600,7 @@ angular.scenario.SpecRunner.prototype.addFutureAction = function(name, behavior, try { behavior.call(self, $window, $document, done); - } catch(e) { + } catch (e) { if (e.type && e.type === 'selector') { done(e.message); } else { @@ -32847,7 +36774,7 @@ angular.scenario.dsl('expect', function() { */ angular.scenario.dsl('using', function() { return function(selector, label) { - this.selector = _jQuery.trim((this.selector||'') + ' ' + selector); + this.selector = _jQuery.trim((this.selector || '') + ' ' + selector); if (angular.isString(label) && label.length) { this.label = label + ' ( ' + this.selector + ' )'; } else { @@ -32883,7 +36810,7 @@ angular.scenario.dsl('binding', function() { */ angular.scenario.dsl('input', function() { var chain = {}; - var supportInputEvent = 'oninput' in document.createElement('div') && msie != 9; + var supportInputEvent = 'oninput' in document.createElement('div') && !(msie && msie <= 11); chain.enter = function(value, event) { return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", @@ -32988,7 +36915,7 @@ angular.scenario.dsl('select', function() { if (option.length) { select.val(value); } else { - option = select.find('option').filter(function(){ + option = select.find('option').filter(function() { return _jQuery(this).text() === value; }); if (!option.length) { @@ -33061,7 +36988,7 @@ angular.scenario.dsl('element', function() { var href = elements.attr('href'); var eventProcessDefault = elements.trigger('click')[0]; - if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) { + if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) { this.application.navigateTo(href, function() { done(); }, done); @@ -33078,7 +37005,7 @@ angular.scenario.dsl('element', function() { var href = elements.attr('href'); var eventProcessDefault = elements.trigger('dblclick')[0]; - if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) { + if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) { this.application.navigateTo(href, function() { done(); }, done); @@ -33460,5 +37387,5 @@ if (config.autotest) { })(window, document); -!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n\n[ng\\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],\n.ng-cloak, .x-ng-cloak,\n.ng-hide {\n display: none !important;\n}\n\nng\\:form {\n display: block;\n}\n\n.ng-animate-block-transitions {\n transition:0s all!important;\n -webkit-transition:0s all!important;\n}\n</style>'); -!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n/* CSS Document */\n\n/** Structure */\nbody {\n font-family: Arial, sans-serif;\n margin: 0;\n font-size: 14px;\n}\n\n#system-error {\n font-size: 1.5em;\n text-align: center;\n}\n\n#json, #xml {\n display: none;\n}\n\n#header {\n position: fixed;\n width: 100%;\n}\n\n#specs {\n padding-top: 50px;\n}\n\n#header .angular {\n font-family: Courier New, monospace;\n font-weight: bold;\n}\n\n#header h1 {\n font-weight: normal;\n float: left;\n font-size: 30px;\n line-height: 30px;\n margin: 0;\n padding: 10px 10px;\n height: 30px;\n}\n\n#application h2,\n#specs h2 {\n margin: 0;\n padding: 0.5em;\n font-size: 1.1em;\n}\n\n#status-legend {\n margin-top: 10px;\n margin-right: 10px;\n}\n\n#header,\n#application,\n.test-info,\n.test-actions li {\n overflow: hidden;\n}\n\n#application {\n margin: 10px;\n}\n\n#application iframe {\n width: 100%;\n height: 758px;\n}\n\n#application .popout {\n float: right;\n}\n\n#application iframe {\n border: none;\n}\n\n.tests li,\n.test-actions li,\n.test-it li,\n.test-it ol,\n.status-display {\n list-style-type: none;\n}\n\n.tests,\n.test-it ol,\n.status-display {\n margin: 0;\n padding: 0;\n}\n\n.test-info {\n margin-left: 1em;\n margin-top: 0.5em;\n border-radius: 8px 0 0 8px;\n -webkit-border-radius: 8px 0 0 8px;\n -moz-border-radius: 8px 0 0 8px;\n cursor: pointer;\n}\n\n.test-info:hover .test-name {\n text-decoration: underline;\n}\n\n.test-info .closed:before {\n content: \'\\25b8\\00A0\';\n}\n\n.test-info .open:before {\n content: \'\\25be\\00A0\';\n font-weight: bold;\n}\n\n.test-it ol {\n margin-left: 2.5em;\n}\n\n.status-display,\n.status-display li {\n float: right;\n}\n\n.status-display li {\n padding: 5px 10px;\n}\n\n.timer-result,\n.test-title {\n display: inline-block;\n margin: 0;\n padding: 4px;\n}\n\n.test-actions .test-title,\n.test-actions .test-result {\n display: table-cell;\n padding-left: 0.5em;\n padding-right: 0.5em;\n}\n\n.test-actions {\n display: table;\n}\n\n.test-actions li {\n display: table-row;\n}\n\n.timer-result {\n width: 4em;\n padding: 0 10px;\n text-align: right;\n font-family: monospace;\n}\n\n.test-it pre,\n.test-actions pre {\n clear: left;\n color: black;\n margin-left: 6em;\n}\n\n.test-describe {\n padding-bottom: 0.5em;\n}\n\n.test-describe .test-describe {\n margin: 5px 5px 10px 2em;\n}\n\n.test-actions .status-pending .test-title:before {\n content: \'\\00bb\\00A0\';\n}\n\n.scrollpane {\n max-height: 20em;\n overflow: auto;\n}\n\n/** Colors */\n\n#header {\n background-color: #F2C200;\n}\n\n#specs h2 {\n border-top: 2px solid #BABAD1;\n}\n\n#specs h2,\n#application h2 {\n background-color: #efefef;\n}\n\n#application {\n border: 1px solid #BABAD1;\n}\n\n.test-describe .test-describe {\n border-left: 1px solid #BABAD1;\n border-right: 1px solid #BABAD1;\n border-bottom: 1px solid #BABAD1;\n}\n\n.status-display {\n border: 1px solid #777;\n}\n\n.status-display .status-pending,\n.status-pending .test-info {\n background-color: #F9EEBC;\n}\n\n.status-display .status-success,\n.status-success .test-info {\n background-color: #B1D7A1;\n}\n\n.status-display .status-failure,\n.status-failure .test-info {\n background-color: #FF8286;\n}\n\n.status-display .status-error,\n.status-error .test-info {\n background-color: black;\n color: white;\n}\n\n.test-actions .status-success .test-title {\n color: #30B30A;\n}\n\n.test-actions .status-failure .test-title {\n color: #DF0000;\n}\n\n.test-actions .status-error .test-title {\n color: black;\n}\n\n.test-actions .timer-result {\n color: #888;\n}\n</style>');
\ No newline at end of file +!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n\n[ng\\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],\n.ng-cloak, .x-ng-cloak,\n.ng-hide:not(.ng-hide-animate) {\n display: none !important;\n}\n\nng\\:form {\n display: block;\n}\n</style>'); +!window.angular.$$csp() && window.angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";\n/* CSS Document */\n\n/** Structure */\nbody {\n font-family: Arial, sans-serif;\n margin: 0;\n font-size: 14px;\n}\n\n#system-error {\n font-size: 1.5em;\n text-align: center;\n}\n\n#json, #xml {\n display: none;\n}\n\n#header {\n position: fixed;\n width: 100%;\n}\n\n#specs {\n padding-top: 50px;\n}\n\n#header .angular {\n font-family: Courier New, monospace;\n font-weight: bold;\n}\n\n#header h1 {\n font-weight: normal;\n float: left;\n font-size: 30px;\n line-height: 30px;\n margin: 0;\n padding: 10px 10px;\n height: 30px;\n}\n\n#application h2,\n#specs h2 {\n margin: 0;\n padding: 0.5em;\n font-size: 1.1em;\n}\n\n#status-legend {\n margin-top: 10px;\n margin-right: 10px;\n}\n\n#header,\n#application,\n.test-info,\n.test-actions li {\n overflow: hidden;\n}\n\n#application {\n margin: 10px;\n}\n\n#application iframe {\n width: 100%;\n height: 758px;\n}\n\n#application .popout {\n float: right;\n}\n\n#application iframe {\n border: none;\n}\n\n.tests li,\n.test-actions li,\n.test-it li,\n.test-it ol,\n.status-display {\n list-style-type: none;\n}\n\n.tests,\n.test-it ol,\n.status-display {\n margin: 0;\n padding: 0;\n}\n\n.test-info {\n margin-left: 1em;\n margin-top: 0.5em;\n border-radius: 8px 0 0 8px;\n -webkit-border-radius: 8px 0 0 8px;\n -moz-border-radius: 8px 0 0 8px;\n cursor: pointer;\n}\n\n.test-info:hover .test-name {\n text-decoration: underline;\n}\n\n.test-info .closed:before {\n content: \'\\25b8\\00A0\';\n}\n\n.test-info .open:before {\n content: \'\\25be\\00A0\';\n font-weight: bold;\n}\n\n.test-it ol {\n margin-left: 2.5em;\n}\n\n.status-display,\n.status-display li {\n float: right;\n}\n\n.status-display li {\n padding: 5px 10px;\n}\n\n.timer-result,\n.test-title {\n display: inline-block;\n margin: 0;\n padding: 4px;\n}\n\n.test-actions .test-title,\n.test-actions .test-result {\n display: table-cell;\n padding-left: 0.5em;\n padding-right: 0.5em;\n}\n\n.test-actions {\n display: table;\n}\n\n.test-actions li {\n display: table-row;\n}\n\n.timer-result {\n width: 4em;\n padding: 0 10px;\n text-align: right;\n font-family: monospace;\n}\n\n.test-it pre,\n.test-actions pre {\n clear: left;\n color: black;\n margin-left: 6em;\n}\n\n.test-describe {\n padding-bottom: 0.5em;\n}\n\n.test-describe .test-describe {\n margin: 5px 5px 10px 2em;\n}\n\n.test-actions .status-pending .test-title:before {\n content: \'\\00bb\\00A0\';\n}\n\n.scrollpane {\n max-height: 20em;\n overflow: auto;\n}\n\n/** Colors */\n\n#header {\n background-color: #F2C200;\n}\n\n#specs h2 {\n border-top: 2px solid #BABAD1;\n}\n\n#specs h2,\n#application h2 {\n background-color: #efefef;\n}\n\n#application {\n border: 1px solid #BABAD1;\n}\n\n.test-describe .test-describe {\n border-left: 1px solid #BABAD1;\n border-right: 1px solid #BABAD1;\n border-bottom: 1px solid #BABAD1;\n}\n\n.status-display {\n border: 1px solid #777;\n}\n\n.status-display .status-pending,\n.status-pending .test-info {\n background-color: #F9EEBC;\n}\n\n.status-display .status-success,\n.status-success .test-info {\n background-color: #B1D7A1;\n}\n\n.status-display .status-failure,\n.status-failure .test-info {\n background-color: #FF8286;\n}\n\n.status-display .status-error,\n.status-error .test-info {\n background-color: black;\n color: white;\n}\n\n.test-actions .status-success .test-title {\n color: #30B30A;\n}\n\n.test-actions .status-failure .test-title {\n color: #DF0000;\n}\n\n.test-actions .status-error .test-title {\n color: black;\n}\n\n.test-actions .timer-result {\n color: #888;\n}\n</style>');
\ No newline at end of file diff --git a/xstatic/pkg/angular/data/angular-touch.js b/xstatic/pkg/angular/data/angular-touch.js index 4157f2a..d53a732 100644 --- a/xstatic/pkg/angular/data/angular-touch.js +++ b/xstatic/pkg/angular/data/angular-touch.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -52,6 +52,20 @@ ngTouch.factory('$swipe', [function() { // The total distance in any direction before we make the call on swipe vs. scroll. var MOVE_BUFFER_RADIUS = 10; + var POINTER_EVENTS = { + 'mouse': { + start: 'mousedown', + move: 'mousemove', + end: 'mouseup' + }, + 'touch': { + start: 'touchstart', + move: 'touchmove', + end: 'touchend', + cancel: 'touchcancel' + } + }; + function getCoordinates(event) { var touches = event.touches && event.touches.length ? event.touches : [event]; var e = (event.changedTouches && event.changedTouches[0]) || @@ -65,6 +79,17 @@ ngTouch.factory('$swipe', [function() { }; } + function getEvents(pointerTypes, eventType) { + var res = []; + angular.forEach(pointerTypes, function(pointerType) { + var eventName = POINTER_EVENTS[pointerType][eventType]; + if (eventName) { + res.push(eventName); + } + }); + return res.join(' '); + } + return { /** * @ngdoc method @@ -73,6 +98,9 @@ ngTouch.factory('$swipe', [function() { * @description * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an * object containing event handlers. + * The pointer types that should be used can be specified via the optional + * third argument, which is an array of strings `'mouse'` and `'touch'`. By default, + * `$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 }`. @@ -95,7 +123,7 @@ ngTouch.factory('$swipe', [function() { * as described above. * */ - bind: function(element, eventHandlers) { + bind: function(element, eventHandlers, pointerTypes) { // Absolute total movement, used to control swipe vs. scroll. var totalX, totalY; // Coordinates of the start position. @@ -105,7 +133,8 @@ ngTouch.factory('$swipe', [function() { // Whether a swipe is active. var active = false; - element.on('touchstart mousedown', function(event) { + pointerTypes = pointerTypes || ['mouse', 'touch']; + element.on(getEvents(pointerTypes, 'start'), function(event) { startCoords = getCoordinates(event); active = true; totalX = 0; @@ -113,13 +142,15 @@ ngTouch.factory('$swipe', [function() { lastPos = startCoords; eventHandlers['start'] && eventHandlers['start'](startCoords, event); }); + var events = getEvents(pointerTypes, 'cancel'); + if (events) { + element.on(events, function(event) { + active = false; + eventHandlers['cancel'] && eventHandlers['cancel'](event); + }); + } - element.on('touchcancel', function(event) { - active = false; - eventHandlers['cancel'] && eventHandlers['cancel'](event); - }); - - element.on('touchmove mousemove', function(event) { + element.on(getEvents(pointerTypes, 'move'), function(event) { if (!active) return; // Android will send a touchcancel if it thinks we're starting to scroll. @@ -153,7 +184,7 @@ ngTouch.factory('$swipe', [function() { } }); - element.on('touchend mouseup', function(event) { + element.on(getEvents(pointerTypes, 'end'), function(event) { if (!active) return; active = false; eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event); @@ -187,13 +218,16 @@ ngTouch.factory('$swipe', [function() { * upon tap. (Event object is available as `$event`) * * @example - <example> + <example module="ngClickExample" deps="angular-touch.js"> <file name="index.html"> <button ng-click="count = count + 1" ng-init="count=0"> Increment </button> count: {{ count }} </file> + <file name="script.js"> + angular.module('ngClickExample', ['ngTouch']); + </file> </example> */ @@ -230,7 +264,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', // // What happens when the browser then generates a click event? // The browser, of course, also detects the tap and fires a click after a delay. This results in - // tapping/clicking twice. So we do "clickbusting" to prevent it. + // tapping/clicking twice. We do "clickbusting" to prevent it. // // How does it work? // We attach global touchstart and click handlers, that run during the capture (early) phase. @@ -253,9 +287,9 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', // encapsulates this ugly logic away from the user. // // Why not just put click handlers on the element? - // We do that too, just to be sure. The problem is that the tap event might have caused the DOM - // to change, so that the click fires in the same position but something else is there now. So - // the handlers are global and care only about coordinates and not elements. + // We do that too, just to be sure. If the tap event caused the DOM to change, + // it is possible another element is now in that position. To take account for these possibly + // distinct elements, the handlers are global and care only about coordinates. // Checks if the coordinates are close enough to be within the region. function hit(x1, y1, x2, y2) { @@ -267,7 +301,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', // Splices out the allowable region from the list after it has been used. function checkAllowableRegions(touchCoordinates, x, y) { for (var i = 0; i < touchCoordinates.length; i += 2) { - if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) { + if (hit(touchCoordinates[i], touchCoordinates[i + 1], x, y)) { touchCoordinates.splice(i, i + 2); return true; // allowable region } @@ -332,7 +366,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', $timeout(function() { // Remove the allowable region. for (var i = 0; i < touchCoordinates.length; i += 2) { - if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) { + if (touchCoordinates[i] == x && touchCoordinates[i + 1] == y) { touchCoordinates.splice(i, i + 2); return; } @@ -372,7 +406,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', tapping = true; tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement. // Hack for Safari, which can target text nodes instead of containers. - if(tapElement.nodeType == 3) { + if (tapElement.nodeType == 3) { tapElement = tapElement.parentNode; } @@ -402,7 +436,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', var e = touches[0].originalEvent || touches[0]; var x = e.clientX; var y = e.clientY; - var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) ); + var dist = Math.sqrt(Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2)); if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) { // Call preventGhostClick so the clickbuster will catch the corresponding click. @@ -462,6 +496,9 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag * too. * + * To disable the mouse click and drag functionality, add `ng-swipe-disable-mouse` to + * the `ng-swipe-left` or `ng-swipe-right` DOM Element. + * * Requires the {@link ngTouch `ngTouch`} module to be installed. * * @element ANY @@ -469,7 +506,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', * upon left swipe. (Event object is available as `$event`) * * @example - <example> + <example module="ngSwipeLeftExample" deps="angular-touch.js"> <file name="index.html"> <div ng-show="!showActions" ng-swipe-left="showActions = true"> Some list content, like an email in the inbox @@ -479,6 +516,9 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', <button ng-click="delete()">Delete</button> </div> </file> + <file name="script.js"> + angular.module('ngSwipeLeftExample', ['ngTouch']); + </file> </example> */ @@ -499,7 +539,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', * upon right swipe. (Event object is available as `$event`) * * @example - <example> + <example module="ngSwipeRightExample" deps="angular-touch.js"> <file name="index.html"> <div ng-show="!showActions" ng-swipe-left="showActions = true"> Some list content, like an email in the inbox @@ -509,6 +549,9 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement', <button ng-click="delete()">Delete</button> </div> </file> + <file name="script.js"> + angular.module('ngSwipeRightExample', ['ngTouch']); + </file> </example> */ @@ -545,6 +588,10 @@ function makeSwipeDirective(directiveName, direction, eventName) { deltaY / deltaX < MAX_VERTICAL_RATIO; } + var pointerTypes = ['touch']; + if (!angular.isDefined(attr['ngSwipeDisableMouse'])) { + pointerTypes.push('mouse'); + } $swipe.bind(element, { 'start': function(coords, event) { startCoords = coords; @@ -561,7 +608,7 @@ function makeSwipeDirective(directiveName, direction, eventName) { }); } } - }); + }, pointerTypes); }; }]); } diff --git a/xstatic/pkg/angular/data/angular.js b/xstatic/pkg/angular/data/angular.js index 2f26bee..f12180f 100644 --- a/xstatic/pkg/angular/data/angular.js +++ b/xstatic/pkg/angular/data/angular.js @@ -1,5 +1,5 @@ /** - * @license AngularJS v1.2.16 + * @license AngularJS v1.3.7 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ @@ -30,139 +30,129 @@ * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. + * @param {function} ErrorConstructor Custom error constructor to be instantiated when returning + * error from returned function, for cases when a particular type of error is useful. * @returns {function(code:string, template:string, ...templateArgs): Error} minErr instance */ -function minErr(module) { - return function () { +function minErr(module, ErrorConstructor) { + ErrorConstructor = ErrorConstructor || Error; + return function() { var code = arguments[0], prefix = '[' + (module ? module + ':' : '') + code + '] ', template = arguments[1], templateArgs = arguments, - stringify = function (obj) { - if (typeof obj === 'function') { - return obj.toString().replace(/ \{[\s\S]*$/, ''); - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else if (typeof obj !== 'string') { - return JSON.stringify(obj); - } - return obj; - }, + message, i; - message = prefix + template.replace(/\{\d+\}/g, function (match) { + message = prefix + template.replace(/\{\d+\}/g, function(match) { var index = +match.slice(1, -1), arg; if (index + 2 < templateArgs.length) { - arg = templateArgs[index + 2]; - if (typeof arg === 'function') { - return arg.toString().replace(/ ?\{[\s\S]*$/, ''); - } else if (typeof arg === 'undefined') { - return 'undefined'; - } else if (typeof arg !== 'string') { - return toJson(arg); - } - return arg; + return toDebugString(templateArgs[index + 2]); } return match; }); - message = message + '\nhttp://errors.angularjs.org/1.2.16/' + + message = message + '\nhttp://errors.angularjs.org/1.3.7/' + (module ? module + '/' : '') + code; for (i = 2; i < arguments.length; i++) { - message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + - encodeURIComponent(stringify(arguments[i])); + message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' + + encodeURIComponent(toDebugString(arguments[i])); } - - return new Error(message); + return new ErrorConstructor(message); }; } /* We need to tell jshint what variables are being exported */ -/* global - -angular, - -msie, - -jqLite, - -jQuery, - -slice, - -push, - -toString, - -ngMinErr, - -_angular, - -angularModule, - -nodeName_, - -uid, - - -lowercase, - -uppercase, - -manualLowercase, - -manualUppercase, - -nodeName_, - -isArrayLike, - -forEach, - -sortedKeys, - -forEachSorted, - -reverseParams, - -nextUid, - -setHashKey, - -extend, - -int, - -inherit, - -noop, - -identity, - -valueFn, - -isUndefined, - -isDefined, - -isObject, - -isString, - -isNumber, - -isDate, - -isArray, - -isFunction, - -isRegExp, - -isWindow, - -isScope, - -isFile, - -isBlob, - -isBoolean, - -trim, - -isElement, - -makeMap, - -map, - -size, - -includes, - -indexOf, - -arrayRemove, - -isLeafNode, - -copy, - -shallowCopy, - -equals, - -csp, - -concat, - -sliceArgs, - -bind, - -toJsonReplacer, - -toJson, - -fromJson, - -toBoolean, - -startingTag, - -tryDecodeURIComponent, - -parseKeyValue, - -toKeyValue, - -encodeUriSegment, - -encodeUriQuery, - -angularInit, - -bootstrap, - -snake_case, - -bindJQuery, - -assertArg, - -assertArgFn, - -assertNotHasOwnProperty, - -getter, - -getBlockElements, - -hasOwnProperty, - +/* global angular: true, + msie: true, + jqLite: true, + jQuery: true, + slice: true, + splice: true, + push: true, + toString: true, + ngMinErr: true, + angularModule: true, + uid: true, + REGEX_STRING_REGEXP: true, + VALIDITY_STATE_PROPERTY: true, + + lowercase: true, + uppercase: true, + manualLowercase: true, + manualUppercase: true, + nodeName_: true, + isArrayLike: true, + forEach: true, + sortedKeys: true, + forEachSorted: true, + reverseParams: true, + nextUid: true, + setHashKey: true, + extend: true, + int: true, + inherit: true, + noop: true, + identity: true, + valueFn: true, + isUndefined: true, + isDefined: true, + isObject: true, + isString: true, + isNumber: true, + isDate: true, + isArray: true, + isFunction: true, + isRegExp: true, + isWindow: true, + isScope: true, + isFile: true, + isFormData: true, + isBlob: true, + isBoolean: true, + isPromiseLike: true, + trim: true, + escapeForRegexp: true, + isElement: true, + makeMap: true, + includes: true, + arrayRemove: true, + copy: true, + shallowCopy: true, + equals: true, + csp: true, + concat: true, + sliceArgs: true, + bind: true, + toJsonReplacer: true, + toJson: true, + fromJson: true, + startingTag: true, + tryDecodeURIComponent: true, + parseKeyValue: true, + toKeyValue: true, + encodeUriSegment: true, + encodeUriQuery: true, + angularInit: true, + bootstrap: true, + getTestability: true, + snake_case: true, + bindJQuery: true, + assertArg: true, + assertArgFn: true, + assertNotHasOwnProperty: true, + getter: true, + getBlockNodes: true, + hasOwnProperty: true, + createMap: true, + + NODE_TYPE_ELEMENT: true, + NODE_TYPE_TEXT: true, + NODE_TYPE_COMMENT: true, + NODE_TYPE_DOCUMENT: true, + NODE_TYPE_DOCUMENT_FRAGMENT: true, */ //////////////////////////////////// @@ -182,30 +172,36 @@ function minErr(module) { * <div doc-module-components="ng"></div> */ +var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; + +// The name of a form control's ValidityState property. +// This is used so that it's possible for internal tests to create mock ValidityStates. +var VALIDITY_STATE_PROPERTY = 'validity'; + /** * @ngdoc function * @name angular.lowercase * @module ng - * @function + * @kind function * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ -var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; +var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function * @name angular.uppercase * @module ng - * @function + * @kind function * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ -var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; +var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; var manualLowercase = function(s) { @@ -231,31 +227,26 @@ if ('i' !== 'I'.toLowerCase()) { } -var /** holds major version number for IE or NaN for real browsers */ - msie, +var + msie, // holds major version number for IE, or NaN if UA is not IE. jqLite, // delay binding since jQuery could be loaded after us. jQuery, // delay binding slice = [].slice, + splice = [].splice, push = [].push, toString = Object.prototype.toString, ngMinErr = minErr('ng'), - - _angular = window.angular, /** @name angular */ angular = window.angular || (window.angular = {}), angularModule, - nodeName_, - uid = ['0', '0', '0']; + uid = 0; /** - * IE 11 changed the format of the UserAgent string. - * See http://msdn.microsoft.com/en-us/library/ms537503.aspx + * documentMode is an IE-only property + * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -if (isNaN(msie)) { - msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); -} +msie = document.documentMode; /** @@ -271,7 +262,7 @@ function isArrayLike(obj) { var length = obj.length; - if (obj.nodeType === 1 && length) { + if (obj.nodeType === NODE_TYPE_ELEMENT && length) { return true; } @@ -283,21 +274,26 @@ function isArrayLike(obj) { * @ngdoc function * @name angular.forEach * @module ng - * @function + * @kind function * * @description * Invokes the `iterator` function once for each item in `obj` collection, which can be either an - * object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` - * is the value of an object property or an array element and `key` is the object property key or - * array element index. Specifying a `context` for the function is optional. + * object or an array. The `iterator` function is invoked with `iterator(value, key, obj)`, where `value` + * is the value of an object property or an array element, `key` is the object property key or + * array element index and obj is the `obj` itself. Specifying a `context` for the function is optional. * * It is worth noting that `.forEach` does not iterate over inherited properties because it filters * using the `hasOwnProperty` method. * + * 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 + * return the value provided. + * ```js var values = {name: 'misko', gender: 'male'}; var log = []; - angular.forEach(values, function(value, key){ + angular.forEach(values, function(value, key) { this.push(key + ': ' + value); }, log); expect(log).toEqual(['name: misko', 'gender: male']); @@ -308,26 +304,31 @@ function isArrayLike(obj) { * @param {Object=} context Object to become context (`this`) for the iterator function. * @returns {Object|Array} Reference to `obj`. */ + function forEach(obj, iterator, context) { - var key; + var key, length; if (obj) { - if (isFunction(obj)){ + if (isFunction(obj)) { for (key in obj) { // Need to check if hasOwnProperty exists, // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); + } + } + } else if (isArray(obj) || isArrayLike(obj)) { + var isPrimitive = typeof obj !== 'object'; + for (key = 0, length = obj.length; key < length; key++) { + if (isPrimitive || key in obj) { + iterator.call(context, obj[key], key, obj); } } } else if (obj.forEach && obj.forEach !== forEach) { - obj.forEach(iterator, context); - } else if (isArrayLike(obj)) { - for (key = 0; key < obj.length; key++) - iterator.call(context, obj[key], key); + obj.forEach(iterator, context, obj); } else { for (key in obj) { if (obj.hasOwnProperty(key)) { - iterator.call(context, obj[key], key); + iterator.call(context, obj[key], key, obj); } } } @@ -336,18 +337,12 @@ function forEach(obj, iterator, context) { } function sortedKeys(obj) { - var keys = []; - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key); - } - } - return keys.sort(); + return Object.keys(obj).sort(); } function forEachSorted(obj, iterator, context) { var keys = sortedKeys(obj); - for ( var i = 0; i < keys.length; i++) { + for (var i = 0; i < keys.length; i++) { iterator.call(context, obj[keys[i]], keys[i]); } return keys; @@ -364,33 +359,17 @@ function reverseParams(iteratorFn) { } /** - * A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric - * characters such as '012ABC'. The reason why we are not using simply a number counter is that - * the number string gets longer over time, and it can also overflow, where as the nextId - * will grow much slower, it is a string, and it will never overflow. + * A consistent way of creating unique IDs in angular. * - * @returns {string} an unique alpha-numeric string + * Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before + * we hit number precision issues in JavaScript. + * + * Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M + * + * @returns {number} an unique alpha-numeric string */ function nextUid() { - var index = uid.length; - var digit; - - while(index) { - index--; - digit = uid[index].charCodeAt(0); - if (digit == 57 /*'9'*/) { - uid[index] = 'A'; - return uid.join(''); - } - if (digit == 90 /*'Z'*/) { - uid[index] = '0'; - } else { - uid[index] = String.fromCharCode(digit + 1); - return uid.join(''); - } - } - uid.unshift('0'); - return uid.join(''); + return ++uid; } @@ -412,11 +391,13 @@ function setHashKey(obj, h) { * @ngdoc function * @name angular.extend * @module ng - * @function + * @kind function * * @description - * Extends the destination object `dst` by copying all of the properties from the `src` object(s) - * to `dst`. You can specify multiple `src` objects. + * 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). * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). @@ -424,15 +405,19 @@ function setHashKey(obj, h) { */ function extend(dst) { var h = dst.$$hashKey; - forEach(arguments, function(obj){ - if (obj !== dst) { - forEach(obj, function(value, key){ - dst[key] = value; - }); + + 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); + setHashKey(dst, h); return dst; } @@ -442,14 +427,14 @@ function int(str) { function inherit(parent, extra) { - return extend(new (extend(function() {}, {prototype:parent}))(), extra); + return extend(Object.create(parent), extra); } /** * @ngdoc function * @name angular.noop * @module ng - * @function + * @kind function * * @description * A function that performs no operations. This function can be useful when writing code in the @@ -469,7 +454,7 @@ noop.$inject = []; * @ngdoc function * @name angular.identity * @module ng - * @function + * @kind function * * @description * A function that returns its first argument. This function is useful when writing code in the @@ -491,7 +476,7 @@ function valueFn(value) {return function() {return value;};} * @ngdoc function * @name angular.isUndefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is undefined. @@ -499,14 +484,14 @@ function valueFn(value) {return function() {return value;};} * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ -function isUndefined(value){return typeof value === 'undefined';} +function isUndefined(value) {return typeof value === 'undefined';} /** * @ngdoc function * @name angular.isDefined * @module ng - * @function + * @kind function * * @description * Determines if a reference is defined. @@ -514,14 +499,14 @@ function isUndefined(value){return typeof value === 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ -function isDefined(value){return typeof value !== 'undefined';} +function isDefined(value) {return typeof value !== 'undefined';} /** * @ngdoc function * @name angular.isObject * @module ng - * @function + * @kind function * * @description * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not @@ -530,14 +515,17 @@ function isDefined(value){return typeof value !== 'undefined';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ -function isObject(value){return value != null && typeof value === 'object';} +function isObject(value) { + // http://jsperf.com/isobject4 + return value !== null && typeof value === 'object'; +} /** * @ngdoc function * @name angular.isString * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `String`. @@ -545,14 +533,14 @@ function isObject(value){return value != null && typeof value === 'object';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ -function isString(value){return typeof value === 'string';} +function isString(value) {return typeof value === 'string';} /** * @ngdoc function * @name angular.isNumber * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Number`. @@ -560,14 +548,14 @@ function isString(value){return typeof value === 'string';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ -function isNumber(value){return typeof value === 'number';} +function isNumber(value) {return typeof value === 'number';} /** * @ngdoc function * @name angular.isDate * @module ng - * @function + * @kind function * * @description * Determines if a value is a date. @@ -575,7 +563,7 @@ function isNumber(value){return typeof value === 'number';} * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ -function isDate(value){ +function isDate(value) { return toString.call(value) === '[object Date]'; } @@ -584,7 +572,7 @@ function isDate(value){ * @ngdoc function * @name angular.isArray * @module ng - * @function + * @kind function * * @description * Determines if a reference is an `Array`. @@ -592,16 +580,13 @@ function isDate(value){ * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -function isArray(value) { - return toString.call(value) === '[object Array]'; -} - +var isArray = Array.isArray; /** * @ngdoc function * @name angular.isFunction * @module ng - * @function + * @kind function * * @description * Determines if a reference is a `Function`. @@ -609,7 +594,7 @@ function isArray(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ -function isFunction(value){return typeof value === 'function';} +function isFunction(value) {return typeof value === 'function';} /** @@ -632,7 +617,7 @@ function isRegExp(value) { * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { - return obj && obj.document && obj.location && obj.alert && obj.setInterval; + return obj && obj.window === obj; } @@ -646,6 +631,11 @@ function isFile(obj) { } +function isFormData(obj) { + return toString.call(obj) === '[object FormData]'; +} + + function isBlob(obj) { return toString.call(obj) === '[object Blob]'; } @@ -656,26 +646,29 @@ function isBoolean(value) { } -var trim = (function() { - // native trim is way faster: http://jsperf.com/angular-trim-test - // but IE doesn't have it... :-( - // TODO: we should move this into IE/ES5 polyfill - if (!String.prototype.trim) { - return function(value) { - return isString(value) ? value.replace(/^\s\s*/, '').replace(/\s\s*$/, '') : value; - }; - } - return function(value) { - return isString(value) ? value.trim() : value; - }; -})(); +function isPromiseLike(obj) { + return obj && isFunction(obj.then); +} + + +var trim = function(value) { + return isString(value) ? value.trim() : value; +}; + +// Copied from: +// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 +// Prereq: s is a string. +var escapeForRegexp = function(s) { + return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). + replace(/\x08/g, '\\x08'); +}; /** * @ngdoc function * @name angular.isElement * @module ng - * @function + * @kind function * * @description * Determines if a reference is a DOM element (or wrapped jQuery element). @@ -693,100 +686,34 @@ function isElement(node) { * @param str 'key1,key2,...' * @returns {object} in the form of {key1:true, key2:true, ...} */ -function makeMap(str){ +function makeMap(str) { var obj = {}, items = str.split(","), i; - for ( i = 0; i < items.length; i++ ) + for (i = 0; i < items.length; i++) obj[ items[i] ] = true; return obj; } -if (msie < 9) { - nodeName_ = function(element) { - element = element.nodeName ? element : element[0]; - return (element.scopeName && element.scopeName != 'HTML') - ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; - }; -} else { - nodeName_ = function(element) { - return element.nodeName ? element.nodeName : element[0].nodeName; - }; -} - - -function map(obj, iterator, context) { - var results = []; - forEach(obj, function(value, index, list) { - results.push(iterator.call(context, value, index, list)); - }); - return results; -} - - -/** - * @description - * Determines the number of elements in an array, the number of properties an object has, or - * the length of a string. - * - * Note: This function is used to augment the Object type in Angular expressions. See - * {@link angular.Object} for more information about Angular arrays. - * - * @param {Object|Array|string} obj Object, array, or string to inspect. - * @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object - * @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. - */ -function size(obj, ownPropsOnly) { - var count = 0, key; - - if (isArray(obj) || isString(obj)) { - return obj.length; - } else if (isObject(obj)){ - for (key in obj) - if (!ownPropsOnly || obj.hasOwnProperty(key)) - count++; - } - - return count; +function nodeName_(element) { + return lowercase(element.nodeName || (element[0] && element[0].nodeName)); } - function includes(array, obj) { - return indexOf(array, obj) != -1; -} - -function indexOf(array, obj) { - if (array.indexOf) return array.indexOf(obj); - - for (var i = 0; i < array.length; i++) { - if (obj === array[i]) return i; - } - return -1; + return Array.prototype.indexOf.call(array, obj) != -1; } function arrayRemove(array, value) { - var index = indexOf(array, value); - if (index >=0) + var index = array.indexOf(value); + if (index >= 0) array.splice(index, 1); return value; } -function isLeafNode (node) { - if (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": - return true; - } - } - return false; -} - /** * @ngdoc function * @name angular.copy * @module ng - * @function + * @kind function * * @description * Creates a deep copy of `source`, which should be an object or an array. @@ -804,9 +731,9 @@ function isLeafNode (node) { * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - <example> + <example module="copyExample"> <file name="index.html"> - <div ng-controller="Controller"> + <div ng-controller="ExampleController"> <form novalidate class="simple-form"> Name: <input type="text" ng-model="user.name" /><br /> E-mail: <input type="email" ng-model="user.email" /><br /> @@ -820,26 +747,27 @@ function isLeafNode (node) { </div> <script> - function Controller($scope) { - $scope.master= {}; + angular.module('copyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.master= {}; - $scope.update = function(user) { - // Example with 1 argument - $scope.master= angular.copy(user); - }; + $scope.update = function(user) { + // Example with 1 argument + $scope.master= angular.copy(user); + }; - $scope.reset = function() { - // Example with 2 arguments - angular.copy($scope.master, $scope.user); - }; + $scope.reset = function() { + // Example with 2 arguments + angular.copy($scope.master, $scope.user); + }; - $scope.reset(); - } + $scope.reset(); + }]); </script> </file> </example> */ -function copy(source, destination){ +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."); @@ -849,52 +777,92 @@ function copy(source, destination){ destination = source; if (source) { if (isArray(source)) { - destination = copy(source, []); + destination = copy(source, [], stackSource, stackDest); } else if (isDate(source)) { destination = new Date(source.getTime()); } else if (isRegExp(source)) { - destination = new RegExp(source.source); + destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); + destination.lastIndex = source.lastIndex; } else if (isObject(source)) { - destination = copy(source, {}); + var emptyObject = Object.create(Object.getPrototypeOf(source)); + destination = copy(source, emptyObject, stackSource, stackDest); } } } else { if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); + + stackSource = stackSource || []; + stackDest = stackDest || []; + + if (isObject(source)) { + var index = stackSource.indexOf(source); + if (index !== -1) return stackDest[index]; + + stackSource.push(source); + stackDest.push(destination); + } + + var result; if (isArray(source)) { destination.length = 0; - for ( var i = 0; i < source.length; i++) { - destination.push(copy(source[i])); + 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); } } else { var h = destination.$$hashKey; - forEach(destination, function(value, key){ - delete destination[key]; - }); - for ( var key in source) { - destination[key] = copy(source[key]); + if (isArray(destination)) { + destination.length = 0; + } else { + forEach(destination, function(value, key) { + delete destination[key]; + }); + } + for (var 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; + } } setHashKey(destination,h); } + } return destination; } /** - * Create a shallow copy of an object + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. */ function shallowCopy(src, dst) { - dst = dst || {}; + if (isArray(src)) { + dst = dst || []; - for(var key in src) { - // shallowCopy is only ever called by $compile nodeLinkFn, which has control over src - // so we don't need to worry about using our custom hasOwnProperty here - if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } } } - return dst; + return dst || src; } @@ -902,7 +870,7 @@ function shallowCopy(src, dst) { * @ngdoc function * @name angular.equals * @module ng - * @function + * @kind function * * @description * Determines if two objects or two values are equivalent. Supports value types, regular @@ -914,7 +882,7 @@ function shallowCopy(src, dst) { * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) - * * Both values represent the same regular expression (In JavasScript, + * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * @@ -937,24 +905,25 @@ function equals(o1, o2) { if (isArray(o1)) { if (!isArray(o2)) return false; if ((length = o1.length) == o2.length) { - for(key=0; key<length; key++) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; } return true; } } else if (isDate(o1)) { - return isDate(o2) && o1.getTime() == o2.getTime(); + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); } else if (isRegExp(o1) && isRegExp(o2)) { return o1.toString() == o2.toString(); } else { if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2)) return false; keySet = {}; - for(key in o1) { + 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) { + for (key in o2) { if (!keySet.hasOwnProperty(key) && key.charAt(0) !== '$' && o2[key] !== undefined && @@ -967,12 +936,25 @@ function equals(o1, o2) { return false; } +var csp = function() { + if (isDefined(csp.isActive_)) return csp.isActive_; + + var active = !!(document.querySelector('[ng-csp]') || + document.querySelector('[data-ng-csp]')); + + if (!active) { + try { + /* jshint -W031, -W054 */ + new Function(''); + /* jshint +W031, +W054 */ + } catch (e) { + active = true; + } + } + + return (csp.isActive_ = active); +}; -function csp() { - return (document.securityPolicy && document.securityPolicy.isActive) || - (document.querySelector && - !!(document.querySelector('[ng-csp]') || document.querySelector('[data-ng-csp]'))); -} function concat(array1, array2, index) { @@ -989,7 +971,7 @@ function sliceArgs(args, startIndex) { * @ngdoc function * @name angular.bind * @module ng - * @function + * @kind function * * @description * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for @@ -1009,7 +991,7 @@ function bind(self, fn) { return curryArgs.length ? function() { return arguments.length - ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) + ? fn.apply(self, concat(curryArgs, arguments, 0)) : fn.apply(self, curryArgs); } : function() { @@ -1027,7 +1009,7 @@ function bind(self, fn) { function toJsonReplacer(key, value) { var val = value; - if (typeof key === 'string' && key.charAt(0) === '$') { + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; @@ -1045,19 +1027,23 @@ function toJsonReplacer(key, value) { * @ngdoc function * @name angular.toJson * @module ng - * @function + * @kind function * * @description - * Serializes input into a JSON-formatted string. Properties with leading $ characters will be + * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. - * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. + * @param {boolean|number=} pretty If set to true, the JSON output will contain newlines and whitespace. + * If set to an integer, the JSON output will contain that many spaces per indentation (the default is 2). * @returns {string|undefined} JSON-ified string representing `obj`. */ function toJson(obj, pretty) { if (typeof obj === 'undefined') return undefined; - return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); } @@ -1065,7 +1051,7 @@ function toJson(obj, pretty) { * @ngdoc function * @name angular.fromJson * @module ng - * @function + * @kind function * * @description * Deserializes a JSON string. @@ -1080,18 +1066,6 @@ function fromJson(json) { } -function toBoolean(value) { - if (typeof value === 'function') { - value = true; - } else if (value && value.length !== 0) { - var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); - } else { - value = false; - } - return value; -} - /** * @returns {string} Returns the string representation of the element. */ @@ -1101,16 +1075,14 @@ function startingTag(element) { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. element.empty(); - } catch(e) {} - // As Per DOM Standards - var TEXT_NODE = 3; + } catch (e) {} var elemHtml = jqLite('<div>').append(element).html(); try { - return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : + return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); - } catch(e) { + } catch (e) { return lowercase(elemHtml); } @@ -1130,7 +1102,7 @@ function startingTag(element) { function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); - } catch(e) { + } catch (e) { // Ignore any invalid uri component } } @@ -1142,15 +1114,15 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}, key_value, key; - forEach((keyValue || "").split('&'), function(keyValue){ - if ( keyValue ) { - key_value = keyValue.split('='); + forEach((keyValue || "").split('&'), function(keyValue) { + if (keyValue) { + key_value = keyValue.replace(/\+/g,'%20').split('='); key = tryDecodeURIComponent(key_value[0]); - if ( isDefined(key) ) { + if (isDefined(key)) { var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; - if (!obj[key]) { + if (!hasOwnProperty.call(obj, key)) { obj[key] = val; - } else if(isArray(obj[key])) { + } else if (isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key],val]; @@ -1214,9 +1186,23 @@ function encodeUriQuery(val, pctEncodeSpaces) { replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). + replace(/%3B/gi, ';'). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } +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))) { + return attr; + } + } + return null; +} /** * @ngdoc directive @@ -1226,6 +1212,11 @@ function encodeUriQuery(val, pctEncodeSpaces) { * @element ANY * @param {angular.Module} ngApp an optional application * {@link angular.module module} name to load. + * @param {boolean=} ngStrictDi if this attribute is present on the app element, the injector will be + * created in "strict-di" mode. This means that the application will fail to invoke functions which + * do not use explicit function annotation (and are thus unsuitable for minification), as described + * in {@link guide/di the Dependency Injection guide}, and useful debugging info will assist in + * tracking down the root of these bugs. * * @description * @@ -1263,48 +1254,109 @@ function encodeUriQuery(val, pctEncodeSpaces) { </file> </example> * + * Using `ngStrictDi`, you would see something like this: + * + <example ng-app-included="true"> + <file name="index.html"> + <div ng-app="ngAppStrictDemo" ng-strict-di> + <div ng-controller="GoodController1"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style (see + script.js for details) + </p> + </div> + + <div ng-controller="GoodController2"> + Name: <input ng-model="name"><br /> + Hello, {{name}}! + + <p>This renders because the controller does not fail to + instantiate, by using explicit annotation style + (see script.js for details) + </p> + </div> + + <div ng-controller="BadController"> + I can add: {{a}} + {{b}} = {{ a+b }} + + <p>The controller could not be instantiated, due to relying + on automatic function annotations (which are disabled in + strict mode). As such, the content of this section is not + interpolated, and there should be an error in your web console. + </p> + </div> + </div> + </file> + <file name="script.js"> + angular.module('ngAppStrictDemo', []) + // BadController will fail to instantiate, due to relying on automatic function annotation, + // rather than an explicit annotation + .controller('BadController', function($scope) { + $scope.a = 1; + $scope.b = 2; + }) + // Unlike BadController, GoodController1 and GoodController2 will not fail to be instantiated, + // due to using explicit annotations using the array style and $inject property, respectively. + .controller('GoodController1', ['$scope', function($scope) { + $scope.a = 1; + $scope.b = 2; + }]) + .controller('GoodController2', GoodController2); + function GoodController2($scope) { + $scope.name = "World"; + } + GoodController2.$inject = ['$scope']; + </file> + <file name="style.css"> + div[ng-controller] { + margin-bottom: 1em; + -webkit-border-radius: 4px; + border-radius: 4px; + border: 1px solid; + padding: .5em; + } + div[ng-controller^=Good] { + border-color: #d6e9c6; + background-color: #dff0d8; + color: #3c763d; + } + div[ng-controller^=Bad] { + border-color: #ebccd1; + background-color: #f2dede; + color: #a94442; + margin-bottom: 0; + } + </file> + </example> */ function angularInit(element, bootstrap) { - var elements = [element], - appElement, + var appElement, module, - names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], - NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; + config = {}; - function append(element) { - element && elements.push(element); - } + // The element `element` has priority over any other element + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; - forEach(names, function(name) { - names[name] = true; - append(document.getElementById(name)); - name = name.replace(':', '\\:'); - if (element.querySelectorAll) { - forEach(element.querySelectorAll('.' + name), append); - forEach(element.querySelectorAll('.' + name + '\\:'), append); - forEach(element.querySelectorAll('[' + name + ']'), append); + if (!appElement && element.hasAttribute && element.hasAttribute(name)) { + appElement = element; + module = element.getAttribute(name); } }); + forEach(ngAttrPrefixes, function(prefix) { + var name = prefix + 'app'; + var candidate; - forEach(elements, function(element) { - if (!appElement) { - var className = ' ' + element.className + ' '; - var match = NG_APP_CLASS_REGEXP.exec(className); - if (match) { - appElement = element; - module = (match[2] || '').replace(/\s+/g, ','); - } else { - forEach(element.attributes, function(attr) { - if (!appElement && names[attr.name]) { - appElement = element; - module = attr.value; - } - }); - } + if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) { + appElement = candidate; + module = candidate.getAttribute(name); } }); if (appElement) { - bootstrap(appElement, module ? [module] : []); + config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + bootstrap(appElement, module ? [module] : [], config); } } @@ -1317,68 +1369,81 @@ function angularInit(element, bootstrap) { * * See: {@link guide/bootstrap Bootstrap} * - * Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. + * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. * They must use {@link ng.directive:ngApp ngApp}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for - * each of the subsequent scripts. This prevents strange results in applications, where otherwise + * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * - * <example name="multi-bootstrap" module="multi-bootstrap"> - * <file name="index.html"> - * <script src="../../../angular.js"></script> - * <div ng-controller="BrokenTable"> - * <table> - * <tr> - * <th ng-repeat="heading in headings">{{heading}}</th> - * </tr> - * <tr ng-repeat="filling in fillings"> - * <td ng-repeat="fill in filling">{{fill}}</td> - * </tr> - * </table> + * ```html + * <!doctype html> + * <html> + * <body> + * <div ng-controller="WelcomeController"> + * {{greeting}} * </div> - * </file> - * <file name="controller.js"> - * var app = angular.module('multi-bootstrap', []) * - * .controller('BrokenTable', function($scope) { - * $scope.headings = ['One', 'Two', 'Three']; - * $scope.fillings = [[1, 2, 3], ['A', 'B', 'C'], [7, 8, 9]]; - * }); - * </file> - * <file name="protractor.js" type="protractor"> - * it('should only insert one table cell for each item in $scope.fillings', function() { - * expect(element.all(by.css('td')).count()) - * .toBe(9); - * }); - * </file> - * </example> + * <script src="angular.js"></script> + * <script> + * var app = angular.module('demo', []) + * .controller('WelcomeController', function($scope) { + * $scope.greeting = 'Welcome!'; + * }); + * angular.bootstrap(document, ['demo']); + * </script> + * </body> + * </html> + * ``` * * @param {DOMElement} element DOM element which is the root of angular application. * @param {Array<String|Function|Array>=} modules an array of modules to load into the application. * Each item in the array should be the name of a predefined module or a (DI annotated) * function that will be invoked by the injector as a run block. * See: {@link angular.module modules} + * @param {Object=} config an object for defining configuration options for the application. The + * following keys are supported: + * + * * `strictDi` - disable automatic function annotation for the application. This is meant to + * assist in finding bugs which break minified code. Defaults to `false`. + * * @returns {auto.$injector} Returns the newly created injector for this app. */ -function bootstrap(element, modules) { +function bootstrap(element, modules, config) { + if (!isObject(config)) config = {}; + var defaultConfig = { + strictDi: false + }; + config = extend(defaultConfig, config); var doBootstrap = function() { element = jqLite(element); if (element.injector()) { var tag = (element[0] === document) ? 'document' : startingTag(element); - throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); + //Encode angle brackets to prevent input from being sanitized to empty string #8683 + throw ngMinErr( + 'btstrpd', + "App Already Bootstrapped with this Element '{0}'", + tag.replace(/</,'<').replace(/>/,'>')); } modules = modules || []; modules.unshift(['$provide', function($provide) { $provide.value('$rootElement', element); }]); + + if (config.debugInfoEnabled) { + // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`. + modules.push(['$compileProvider', function($compileProvider) { + $compileProvider.debugInfoEnabled(true); + }]); + } + modules.unshift('ng'); - var injector = createInjector(modules); - injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', - function(scope, element, compile, injector, animate) { + var injector = createInjector(modules, config.strictDi); + injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', + function bootstrapApply(scope, element, compile, injector) { scope.$apply(function() { element.data('$injector', injector); compile(element)(scope); @@ -1388,8 +1453,14 @@ function bootstrap(element, modules) { return injector; }; + var NG_ENABLE_DEBUG_INFO = /^NG_ENABLE_DEBUG_INFO!/; var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; + if (window && NG_ENABLE_DEBUG_INFO.test(window.name)) { + config.debugInfoEnabled = true; + window.name = window.name.replace(NG_ENABLE_DEBUG_INFO, ''); + } + if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { return doBootstrap(); } @@ -1403,19 +1474,62 @@ function bootstrap(element, modules) { }; } +/** + * @ngdoc function + * @name angular.reloadWithDebugInfo + * @module ng + * @description + * Use this function to reload the current application with debug information turned on. + * This takes precedence over a call to `$compileProvider.debugInfoEnabled(false)`. + * + * See {@link ng.$compileProvider#debugInfoEnabled} for more. + */ +function reloadWithDebugInfo() { + window.name = 'NG_ENABLE_DEBUG_INFO!' + window.name; + window.location.reload(); +} + +/** + * @name angular.getTestability + * @module ng + * @description + * Get the testability service for the instance of Angular on the given + * element. + * @param {DOMElement} element DOM element which is the root of angular application. + */ +function getTestability(rootElement) { + var injector = angular.element(rootElement).injector(); + if (!injector) { + throw ngMinErr('test', + 'no injector found for element argument to getTestability'); + } + return injector.get('$$testability'); +} + var SNAKE_CASE_REGEXP = /[A-Z]/g; -function snake_case(name, separator){ +function snake_case(name, separator) { separator = separator || '_'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } +var bindJQueryFired = false; +var skipDestroyOnNextJQueryCleanData; function bindJQuery() { + var originalCleanData; + + if (bindJQueryFired) { + return; + } + // bind to jQuery if present; jQuery = window.jQuery; - // reset to jQuery or default to us. - if (jQuery) { + // 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 + // versions. It will not work for sure with jQuery <1.7, though. + if (jQuery && jQuery.fn.on) { jqLite = jQuery; extend(jQuery.fn, { scope: JQLitePrototype.scope, @@ -1424,15 +1538,33 @@ function bindJQuery() { injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); - // Method signature: - // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) - jqLitePatchJQueryRemove('remove', true, true, false); - jqLitePatchJQueryRemove('empty', false, false, false); - jqLitePatchJQueryRemove('html', false, false, true); + + // All nodes removed from the DOM via various jQuery APIs like .remove() + // are passed through jQuery.cleanData. Monkey-patch this method to fire + // the $destroy event on all removed nodes. + originalCleanData = jQuery.cleanData; + jQuery.cleanData = function(elems) { + var events; + if (!skipDestroyOnNextJQueryCleanData) { + for (var i = 0, elem; (elem = elems[i]) != null; i++) { + events = jQuery._data(elem, "events"); + if (events && events.$destroy) { + jQuery(elem).triggerHandler('$destroy'); + } + } + } else { + skipDestroyOnNextJQueryCleanData = false; + } + originalCleanData(elems); + }; } else { jqLite = JQLite; } + angular.element = jqLite; + + // Prevent double-proxying. + bindJQueryFired = true; } /** @@ -1451,7 +1583,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { } assertArg(isFunction(arg), name, 'not a function, got ' + - (arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); + (arg && typeof arg === 'object' ? arg.constructor.name || 'Object' : typeof arg)); return arg; } @@ -1496,27 +1628,46 @@ 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 {DOMElement} object containing the elements + * @returns {jqLite} jqLite collection containing the nodes */ -function getBlockElements(nodes) { - var startNode = nodes[0], - endNode = nodes[nodes.length - 1]; - if (startNode === endNode) { - return jqLite(startNode); - } - - var element = startNode; - var elements = [element]; +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. + var node = nodes[0]; + var endNode = nodes[nodes.length - 1]; + var blockNodes = [node]; do { - element = element.nextSibling; - if (!element) break; - elements.push(element); - } while (element !== endNode); + node = node.nextSibling; + if (!node) break; + blockNodes.push(node); + } while (node !== endNode); - return jqLite(elements); + return jqLite(blockNodes); } + +/** + * Creates a new object without a prototype. This object is useful for lookup without having to + * guard against prototypically inherited properties via hasOwnProperty. + * + * Related micro-benchmarks: + * - http://jsperf.com/object-create2 + * - http://jsperf.com/proto-map-lookup/2 + * - http://jsperf.com/for-in-vs-object-keys2 + * + * @returns {Object} + */ +function createMap() { + return Object.create(null); +} + +var NODE_TYPE_ELEMENT = 1; +var NODE_TYPE_TEXT = 3; +var NODE_TYPE_COMMENT = 8; +var NODE_TYPE_DOCUMENT = 9; +var NODE_TYPE_DOCUMENT_FRAGMENT = 11; + /** * @ngdoc type * @name angular.Module @@ -1561,7 +1712,7 @@ function setupModuleLoader(window) { * * # Module * - * A module is a collection of services, directives, filters, and configuration information. + * A module is a collection of services, directives, controllers, filters, and configuration information. * `angular.module` is used to configure the {@link auto.$injector $injector}. * * ```js @@ -1589,9 +1740,9 @@ function setupModuleLoader(window) { * {@link angular.bootstrap} to simplify this process for you. * * @param {!string} name The name of the module to create or retrieve. -<<<<<* @param {!Array.<string>=} requires If specified then new module is being created. If ->>>>>* unspecified then the module is being retrieved for further configuration. - * @param {Function} configFn Optional configuration function for the module. Same as + * @param {!Array.<string>=} requires If specified then new module is being created. If + * 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. */ @@ -1617,21 +1768,25 @@ function setupModuleLoader(window) { var invokeQueue = []; /** @type {!Array.<Function>} */ + var configBlocks = []; + + /** @type {!Array.<Function>} */ var runBlocks = []; - var config = invokeLater('$injector', 'invoke'); + var config = invokeLater('$injector', 'invoke', 'push', configBlocks); /** @type {angular.Module} */ var moduleInstance = { // Private state _invokeQueue: invokeQueue, + _configBlocks: configBlocks, _runBlocks: runBlocks, /** * @ngdoc property * @name angular.Module#requires * @module ng - * @returns {Array.<string>} List of module names which must be loaded before this module. + * * @description * Holds the list of modules which the injector will load before the current module is * loaded. @@ -1642,8 +1797,9 @@ function setupModuleLoader(window) { * @ngdoc property * @name angular.Module#name * @module ng - * @returns {string} Name of the module. + * * @description + * Name of the module. */ name: name, @@ -1734,7 +1890,7 @@ function setupModuleLoader(window) { * }) * ``` * - * See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and + * See {@link ng.$animateProvider#register $animateProvider.register()} and * {@link ngAnimate ngAnimate module} for more information. */ animation: invokeLater('$animateProvider', 'register'), @@ -1783,6 +1939,8 @@ function setupModuleLoader(window) { * configuration. * @description * Use this method to register work which needs to be performed on module loading. + * For more about how to configure services, see + * {@link providers#provider-recipe Provider Recipe}. */ config: config, @@ -1806,7 +1964,7 @@ function setupModuleLoader(window) { config(configFn); } - return moduleInstance; + return moduleInstance; /** * @param {string} provider @@ -1814,9 +1972,10 @@ function setupModuleLoader(window) { * @param {String=} insertMethod * @returns {angular.Module} */ - function invokeLater(provider, method, insertMethod) { + function invokeLater(provider, method, insertMethod, queue) { + if (!queue) queue = invokeQueue; return function() { - invokeQueue[insertMethod || 'push']([provider, method, arguments]); + queue[insertMethod || 'push']([provider, method, arguments]); return moduleInstance; }; } @@ -1826,81 +1985,119 @@ function setupModuleLoader(window) { } -/* global - angularModule: true, - version: true, - - $LocaleProvider, - $CompileProvider, - - htmlAnchorDirective, - inputDirective, - inputDirective, - formDirective, - scriptDirective, - selectDirective, - styleDirective, - optionDirective, - ngBindDirective, - ngBindHtmlDirective, - ngBindTemplateDirective, - ngClassDirective, - ngClassEvenDirective, - ngClassOddDirective, - ngCspDirective, - ngCloakDirective, - ngControllerDirective, - ngFormDirective, - ngHideDirective, - ngIfDirective, - ngIncludeDirective, - ngIncludeFillContentDirective, - ngInitDirective, - ngNonBindableDirective, - ngPluralizeDirective, - ngRepeatDirective, - ngShowDirective, - ngStyleDirective, - ngSwitchDirective, - ngSwitchWhenDirective, - ngSwitchDefaultDirective, - ngOptionsDirective, - ngTranscludeDirective, - ngModelDirective, - ngListDirective, - ngChangeDirective, - requiredDirective, - requiredDirective, - ngValueDirective, - ngAttributeAliasDirectives, - ngEventDirectives, - - $AnchorScrollProvider, - $AnimateProvider, - $BrowserProvider, - $CacheFactoryProvider, - $ControllerProvider, - $DocumentProvider, - $ExceptionHandlerProvider, - $FilterProvider, - $InterpolateProvider, - $IntervalProvider, - $HttpProvider, - $HttpBackendProvider, - $LocationProvider, - $LogProvider, - $ParseProvider, - $RootScopeProvider, - $QProvider, - $$SanitizeUriProvider, - $SceProvider, - $SceDelegateProvider, - $SnifferProvider, - $TemplateCacheProvider, - $TimeoutProvider, - $$RAFProvider, - $$AsyncCallbackProvider, - $WindowProvider +/* 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 '<<already seen>>'; + + seen.push(val); + } + return val; + }); +} + +function toDebugString(obj) { + if (typeof obj === 'function') { + return obj.toString().replace(/ \{[\s\S]*$/, ''); + } else if (typeof obj === 'undefined') { + return 'undefined'; + } else if (typeof obj !== 'string') { + return serializeObject(obj); + } + return obj; +} + +/* global angularModule: true, + version: true, + + $LocaleProvider, + $CompileProvider, + + htmlAnchorDirective, + inputDirective, + inputDirective, + formDirective, + scriptDirective, + selectDirective, + styleDirective, + optionDirective, + ngBindDirective, + ngBindHtmlDirective, + ngBindTemplateDirective, + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective, + ngCspDirective, + ngCloakDirective, + ngControllerDirective, + ngFormDirective, + ngHideDirective, + ngIfDirective, + ngIncludeDirective, + ngIncludeFillContentDirective, + ngInitDirective, + ngNonBindableDirective, + ngPluralizeDirective, + ngRepeatDirective, + ngShowDirective, + ngStyleDirective, + ngSwitchDirective, + ngSwitchWhenDirective, + ngSwitchDefaultDirective, + ngOptionsDirective, + ngTranscludeDirective, + ngModelDirective, + ngListDirective, + ngChangeDirective, + patternDirective, + patternDirective, + requiredDirective, + requiredDirective, + minlengthDirective, + minlengthDirective, + maxlengthDirective, + maxlengthDirective, + ngValueDirective, + ngModelOptionsDirective, + ngAttributeAliasDirectives, + ngEventDirectives, + + $AnchorScrollProvider, + $AnimateProvider, + $BrowserProvider, + $CacheFactoryProvider, + $ControllerProvider, + $DocumentProvider, + $ExceptionHandlerProvider, + $FilterProvider, + $InterpolateProvider, + $IntervalProvider, + $HttpProvider, + $HttpBackendProvider, + $LocationProvider, + $LogProvider, + $ParseProvider, + $RootScopeProvider, + $QProvider, + $$QProvider, + $$SanitizeUriProvider, + $SceProvider, + $SceDelegateProvider, + $SnifferProvider, + $TemplateCacheProvider, + $TemplateRequestProvider, + $$TestabilityProvider, + $TimeoutProvider, + $$RAFProvider, + $$AsyncCallbackProvider, + $WindowProvider, + $$jqLiteProvider */ @@ -1919,15 +2116,15 @@ function setupModuleLoader(window) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.2.16', // all of these placeholder strings will be replaced by grunt's + full: '1.3.7', // all of these placeholder strings will be replaced by grunt's major: 1, // package task - minor: 2, - dot: 16, - codeName: 'badger-enumeration' + minor: 3, + dot: 7, + codeName: 'leaky-obstruction' }; -function publishExternalAPI(angular){ +function publishExternalAPI(angular) { extend(angular, { 'bootstrap': bootstrap, 'copy': copy, @@ -1936,11 +2133,11 @@ function publishExternalAPI(angular){ 'element': jqLite, 'forEach': forEach, 'injector': createInjector, - 'noop':noop, - 'bind':bind, + 'noop': noop, + 'bind': bind, 'toJson': toJson, 'fromJson': fromJson, - 'identity':identity, + 'identity': identity, 'isUndefined': isUndefined, 'isDefined': isDefined, 'isString': isString, @@ -1954,8 +2151,10 @@ function publishExternalAPI(angular){ 'lowercase': lowercase, 'uppercase': uppercase, 'callbacks': {counter: 0}, + 'getTestability': getTestability, '$$minErr': minErr, - '$$csp': csp + '$$csp': csp, + 'reloadWithDebugInfo': reloadWithDebugInfo }); angularModule = setupModuleLoader(window); @@ -2007,9 +2206,16 @@ function publishExternalAPI(angular){ ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, + pattern: patternDirective, + ngPattern: patternDirective, required: requiredDirective, ngRequired: requiredDirective, - ngValue: ngValueDirective + minlength: minlengthDirective, + ngMinlength: minlengthDirective, + maxlength: maxlengthDirective, + ngMaxlength: maxlengthDirective, + ngValue: ngValueDirective, + ngModelOptions: ngModelOptionsDirective }). directive({ ngInclude: ngIncludeFillContentDirective @@ -2034,25 +2240,28 @@ function publishExternalAPI(angular){ $parse: $ParseProvider, $rootScope: $RootScopeProvider, $q: $QProvider, + $$q: $$QProvider, $sce: $SceProvider, $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, + $$testability: $$TestabilityProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, - $$asyncCallback : $$AsyncCallbackProvider + $$asyncCallback: $$AsyncCallbackProvider, + $$jqLite: $$jqLiteProvider }); } ]); } -/* global - - -JQLitePrototype, - -addEventListenerFn, - -removeEventListenerFn, - -BOOLEAN_ATTR +/* global JQLitePrototype: true, + addEventListenerFn: true, + removeEventListenerFn: true, + BOOLEAN_ATTR: true, + ALIASED_ATTR: true, */ ////////////////////////////////// @@ -2063,7 +2272,7 @@ function publishExternalAPI(angular){ * @ngdoc function * @name angular.element * @module ng - * @function + * @kind function * * @description * Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. @@ -2087,13 +2296,14 @@ function publishExternalAPI(angular){ * - [`addClass()`](http://api.jquery.com/addClass/) * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) - * - [`attr()`](http://api.jquery.com/attr/) + * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData * - [`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/) + * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()` * - [`data()`](http://api.jquery.com/data/) + * - [`detach()`](http://api.jquery.com/detach/) * - [`empty()`](http://api.jquery.com/empty/) * - [`eq()`](http://api.jquery.com/eq/) * - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name @@ -2134,10 +2344,12 @@ function publishExternalAPI(angular){ * `'ngModel'`). * - `injector()` - retrieves the injector of the current element or its parent. * - `scope()` - retrieves the {@link ng.$rootScope.Scope scope} of the current - * element or its parent. + * element or its parent. Requires {@link guide/production#disabling-debug-data Debug Data} to + * be enabled. * - `isolateScope()` - retrieves an isolate {@link ng.$rootScope.Scope scope} if one is attached directly to the * current element. This getter should be used only on elements that contain a directive which starts a new isolate * scope. Calling `scope()` on this element always returns the original non-isolate scope. + * Requires {@link guide/production#disabling-debug-data Debug Data} to be enabled. * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * @@ -2145,20 +2357,21 @@ function publishExternalAPI(angular){ * @returns {Object} jQuery object. */ +JQLite.expando = 'ng339'; + var jqCache = JQLite.cache = {}, - jqName = JQLite.expando = 'ng-' + new Date().getTime(), jqId = 1, - addEventListenerFn = (window.document.addEventListener - ? function(element, type, fn) {element.addEventListener(type, fn, false);} - : function(element, type, fn) {element.attachEvent('on' + type, fn);}), - removeEventListenerFn = (window.document.removeEventListener - ? function(element, type, fn) {element.removeEventListener(type, fn, false); } - : function(element, type, fn) {element.detachEvent('on' + type, fn); }); + addEventListenerFn = function(element, type, fn) { + element.addEventListener(type, fn, false); + }, + removeEventListenerFn = function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; /* * !!! This is an undocumented "private" function !!! */ -var jqData = JQLite._data = function(node) { +JQLite._data = function(node) { //jQuery always returns an object on cache miss return this.cache[node[this.expando]] || {}; }; @@ -2168,6 +2381,7 @@ function jqNextId() { return ++jqId; } var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; var MOZ_HACK_REGEXP = /^moz([A-Z])/; +var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; var jqLiteMinErr = minErr('jqLite'); /** @@ -2183,49 +2397,6 @@ function camelCase(name) { replace(MOZ_HACK_REGEXP, 'Moz$1'); } -///////////////////////////////////////////// -// jQuery mutation patch -// -// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a -// $destroy event on all DOM nodes being removed. -// -///////////////////////////////////////////// - -function jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { - var originalJqFn = jQuery.fn[name]; - originalJqFn = originalJqFn.$original || originalJqFn; - removePatch.$original = originalJqFn; - jQuery.fn[name] = removePatch; - - function removePatch(param) { - // jshint -W040 - var list = filterElems && param ? [this.filter(param)] : [this], - fireEvent = dispatchThis, - set, setIndex, setLength, - element, childIndex, childLength, children; - - if (!getterIfNoArguments || param != null) { - while(list.length) { - set = list.shift(); - for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { - element = jqLite(set[setIndex]); - if (fireEvent) { - element.triggerHandler('$destroy'); - } else { - fireEvent = !fireEvent; - } - for(childIndex = 0, childLength = (children = element.children()).length; - childIndex < childLength; - childIndex++) { - list.push(jQuery(children[childIndex])); - } - } - } - } - return originalJqFn.apply(this, arguments); - } -} - var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/; var HTML_REGEXP = /<|&#?\w+;/; var TAG_NAME_REGEXP = /<([\w:]+)/; @@ -2245,26 +2416,32 @@ wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; + function jqLiteIsTextNode(html) { return !HTML_REGEXP.test(html); } +function jqLiteAcceptsData(node) { + // The window object can accept data but has no nodeType + // Otherwise we are only interested in elements (1) and documents (9) + var nodeType = node.nodeType; + return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT; +} + function jqLiteBuildFragment(html, context) { - var elem, tmp, tag, wrap, + var tmp, tag, wrap, fragment = context.createDocumentFragment(), - nodes = [], i, j, jj; + nodes = [], i; if (jqLiteIsTextNode(html)) { // Convert non-html into a text node nodes.push(context.createTextNode(html)); } else { - tmp = fragment.appendChild(context.createElement('div')); // Convert html into DOM nodes + tmp = tmp || fragment.appendChild(context.createElement("div")); tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = '<div> </div>' + - wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; - tmp.removeChild(tmp.firstChild); + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -2272,7 +2449,7 @@ function jqLiteBuildFragment(html, context) { tmp = tmp.lastChild; } - for (j=0, jj=tmp.childNodes.length; j<jj; ++j) nodes.push(tmp.childNodes[j]); + nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; tmp.textContent = ""; @@ -2281,7 +2458,11 @@ function jqLiteBuildFragment(html, context) { // Remove wrapper from fragment fragment.textContent = ""; fragment.innerHTML = ""; // Clear inner HTML - return nodes; + forEach(nodes, function(node) { + fragment.appendChild(node); + }); + + return fragment; } function jqLiteParseHTML(html, context) { @@ -2292,7 +2473,11 @@ function jqLiteParseHTML(html, context) { return [context.createElement(parsed[1])]; } - return jqLiteBuildFragment(html, context); + if ((parsed = jqLiteBuildFragment(html, context))) { + return parsed.childNodes; + } + + return []; } ///////////////////////////////////////////// @@ -2300,20 +2485,22 @@ function JQLite(element) { if (element instanceof JQLite) { return element; } + + var argIsString; + if (isString(element)) { element = trim(element); + argIsString = true; } if (!(this instanceof JQLite)) { - if (isString(element) && element.charAt(0) != '<') { + if (argIsString && element.charAt(0) != '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); } - if (isString(element)) { + if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); - var fragment = jqLite(document.createDocumentFragment()); - fragment.append(this); } else { jqLiteAddNodes(this, element); } @@ -2323,94 +2510,106 @@ function jqLiteClone(element) { return element.cloneNode(true); } -function jqLiteDealoc(element){ - jqLiteRemoveData(element); - for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { - jqLiteDealoc(children[i]); +function jqLiteDealoc(element, onlyDescendants) { + if (!onlyDescendants) jqLiteRemoveData(element); + + if (element.querySelectorAll) { + var descendants = element.querySelectorAll('*'); + for (var i = 0, l = descendants.length; i < l; i++) { + jqLiteRemoveData(descendants[i]); + } } } function jqLiteOff(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var handle = expandoStore && expandoStore.handle; if (!handle) return; //no listeners registered - if (isUndefined(type)) { - forEach(events, function(eventHandler, type) { - removeEventListenerFn(element, type, eventHandler); + if (!type) { + for (type in events) { + if (type !== '$destroy') { + removeEventListenerFn(element, type, handle); + } delete events[type]; - }); + } } else { forEach(type.split(' '), function(type) { - if (isUndefined(fn)) { - removeEventListenerFn(element, type, events[type]); - delete events[type]; - } else { - arrayRemove(events[type] || [], fn); + if (isDefined(fn)) { + var listenerFns = events[type]; + arrayRemove(listenerFns || [], fn); + if (listenerFns && listenerFns.length > 0) { + return; + } } + + removeEventListenerFn(element, type, handle); + delete events[type]; }); } } function jqLiteRemoveData(element, name) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId]; + var expandoId = element.ng339; + var expandoStore = expandoId && jqCache[expandoId]; if (expandoStore) { if (name) { - delete jqCache[expandoId].data[name]; + delete expandoStore.data[name]; return; } if (expandoStore.handle) { - expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); + if (expandoStore.events.$destroy) { + expandoStore.handle({}, '$destroy'); + } jqLiteOff(element); } delete jqCache[expandoId]; - element[jqName] = undefined; // ie does not allow deletion of attributes on elements. + element.ng339 = undefined; // don't delete DOM expandos. IE and Chrome don't like it } } -function jqLiteExpandoStore(element, key, value) { - var expandoId = element[jqName], - expandoStore = jqCache[expandoId || -1]; - if (isDefined(value)) { - if (!expandoStore) { - element[jqName] = expandoId = jqNextId(); - expandoStore = jqCache[expandoId] = {}; - } - expandoStore[key] = value; - } else { - return expandoStore && expandoStore[key]; +function jqLiteExpandoStore(element, createIfNecessary) { + var expandoId = element.ng339, + expandoStore = expandoId && jqCache[expandoId]; + + if (createIfNecessary && !expandoStore) { + element.ng339 = expandoId = jqNextId(); + expandoStore = jqCache[expandoId] = {events: {}, data: {}, handle: undefined}; } + + return expandoStore; } + function jqLiteData(element, key, value) { - var data = jqLiteExpandoStore(element, 'data'), - isSetter = isDefined(value), - keyDefined = !isSetter && isDefined(key), - isSimpleGetter = keyDefined && !isObject(key); + if (jqLiteAcceptsData(element)) { - if (!data && !isSimpleGetter) { - jqLiteExpandoStore(element, 'data', data = {}); - } + var isSimpleSetter = isDefined(value); + var isSimpleGetter = !isSimpleSetter && key && !isObject(key); + var massGetter = !key; + var expandoStore = jqLiteExpandoStore(element, !isSimpleGetter); + var data = expandoStore && expandoStore.data; - if (isSetter) { - data[key] = value; - } else { - if (keyDefined) { - if (isSimpleGetter) { - // don't create data in this case. - return data && data[key]; + if (isSimpleSetter) { // data('key', value) + data[key] = value; + } else { + if (massGetter) { // data() + return data; } else { - extend(data, key); + if (isSimpleGetter) { // data('key') + // don't force creation of expandoStore if it doesn't exist yet + return data && data[key]; + } else { // mass-setter: data({key1: val1, key2: val2}) + extend(data, key); + } } - } else { - return data; } } } @@ -2418,7 +2617,7 @@ function jqLiteData(element, key, value) { function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf( " " + selector + " " ) > -1); + indexOf(" " + selector + " ") > -1); } function jqLiteRemoveClass(element, cssClasses) { @@ -2449,53 +2648,84 @@ function jqLiteAddClass(element, cssClasses) { } } + function jqLiteAddNodes(root, elements) { + // THIS CODE IS VERY HOT. Don't make changes without benchmarking. + if (elements) { - elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) - ? elements - : [ elements ]; - for(var i=0; i < elements.length; i++) { - root.push(elements[i]); + + // if a Node (the most common case) + if (elements.nodeType) { + root[root.length++] = elements; + } else { + var length = elements.length; + + // if an Array or NodeList and not a Window + if (typeof length === 'number' && elements.window !== elements) { + if (length) { + for (var i = 0; i < length; i++) { + root[root.length++] = elements[i]; + } + } + } else { + root[root.length++] = elements; + } } } } + function jqLiteController(element, name) { - return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); + return jqLiteInheritedData(element, '$' + (name || 'ngController') + 'Controller'); } function jqLiteInheritedData(element, name, value) { - element = jqLite(element); - // if element is the document object work with the html element instead // this makes $(document).scope() possible - if(element[0].nodeType == 9) { - element = element.find('html'); + if (element.nodeType == NODE_TYPE_DOCUMENT) { + element = element.documentElement; } var names = isArray(name) ? name : [name]; - while (element.length) { - var node = element[0]; + while (element) { for (var i = 0, ii = names.length; i < ii; i++) { - if ((value = element.data(names[i])) !== undefined) return value; + if ((value = jqLite.data(element, names[i])) !== undefined) return value; } // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. - element = jqLite(node.parentNode || (node.nodeType === 11 && node.host)); + element = element.parentNode || (element.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT && element.host); } } function jqLiteEmpty(element) { - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); while (element.firstChild) { element.removeChild(element.firstChild); } } +function jqLiteRemove(element, keepData) { + if (!keepData) jqLiteDealoc(element); + var parent = element.parentNode; + if (parent) parent.removeChild(element); +} + + +function jqLiteDocumentLoaded(action, win) { + win = win || window; + if (win.document.readyState === 'complete') { + // Force the action to be run async for consistent behaviour + // from the action's point of view + // i.e. it will definitely not be in a $apply + win.setTimeout(action); + } else { + // No need to unbind this handler as load is only ever called once + jqLite(win).on('load', action); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// @@ -2509,8 +2739,8 @@ var JQLitePrototype = JQLite.prototype = { fn(); } - // check if document already is loaded - if (document.readyState === 'complete'){ + // check if document is already loaded + if (document.readyState === 'complete') { setTimeout(trigger); } else { this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 @@ -2522,7 +2752,7 @@ var JQLitePrototype = JQLite.prototype = { }, toString: function() { var value = []; - forEach(this, function(e){ value.push('' + e);}); + forEach(this, function(e) { value.push('' + e);}); return '[' + value.join(', ') + ']'; }, @@ -2547,29 +2777,48 @@ forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), }); var BOOLEAN_ELEMENTS = {}; forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { - BOOLEAN_ELEMENTS[uppercase(value)] = true; + BOOLEAN_ELEMENTS[value] = true; }); +var ALIASED_ATTR = { + 'ngMinlength': 'minlength', + 'ngMaxlength': 'maxlength', + 'ngMin': 'min', + 'ngMax': 'max', + 'ngPattern': 'pattern' +}; function getBooleanAttrName(element, name) { // check dom last since we will most likely fail on name var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; // booleanAttr is here twice to minimize DOM access - return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; + return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr; +} + +function getAliasedAttrName(element, name) { + var nodeName = element.nodeName; + return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name]; } forEach({ data: jqLiteData, + removeData: jqLiteRemoveData +}, function(fn, name) { + JQLite[name] = fn; +}); + +forEach({ + data: jqLiteData, inheritedData: jqLiteInheritedData, scope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); + return jqLite.data(element, '$scope') || jqLiteInheritedData(element.parentNode || element, ['$isolateScope', '$scope']); }, isolateScope: function(element) { // Can't use jqLiteData here directly so we stay compatible with jQuery! - return jqLite(element).data('$isolateScope') || jqLite(element).data('$isolateScopeNoTemplate'); + return jqLite.data(element, '$isolateScope') || jqLite.data(element, '$isolateScopeNoTemplate'); }, controller: jqLiteController, @@ -2578,7 +2827,7 @@ forEach({ return jqLiteInheritedData(element, '$injector'); }, - removeAttr: function(element,name) { + removeAttr: function(element, name) { element.removeAttribute(name); }, @@ -2590,26 +2839,11 @@ forEach({ if (isDefined(value)) { element.style[name] = value; } else { - var val; - - if (msie <= 8) { - // this is some IE specific weirdness that jQuery 1.6.4 does not sure why - val = element.currentStyle && element.currentStyle[name]; - if (val === '') val = 'auto'; - } - - val = val || element.style[name]; - - if (msie <= 8) { - // jquery weirdness :-/ - val = (val === '') ? undefined : val; - } - - return val; + return element.style[name]; } }, - attr: function(element, name, value){ + attr: function(element, name, value) { var lowercasedName = lowercase(name); if (BOOLEAN_ATTR[lowercasedName]) { if (isDefined(value)) { @@ -2622,7 +2856,7 @@ forEach({ } } else { return (element[name] || - (element.attributes.getNamedItem(name)|| noop).specified) + (element.attributes.getNamedItem(name) || noop).specified) ? lowercasedName : undefined; } @@ -2646,31 +2880,23 @@ forEach({ }, text: (function() { - var NODE_TYPE_TEXT_PROPERTY = []; - if (msie < 9) { - NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ - } else { - NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ - NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ - } getText.$dv = ''; return getText; function getText(element, value) { - var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType]; if (isUndefined(value)) { - return textProp ? element[textProp] : ''; + var nodeType = element.nodeType; + return (nodeType === NODE_TYPE_ELEMENT || nodeType === NODE_TYPE_TEXT) ? element.textContent : ''; } - element[textProp] = value; + element.textContent = value; } })(), val: function(element, value) { if (isUndefined(value)) { - if (nodeName_(element) === 'SELECT' && element.multiple) { + if (element.multiple && nodeName_(element) === 'select') { var result = []; - forEach(element.options, function (option) { + forEach(element.options, function(option) { if (option.selected) { result.push(option.value || option.text); } @@ -2686,19 +2912,18 @@ forEach({ if (isUndefined(value)) { return element.innerHTML; } - for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { - jqLiteDealoc(childNodes[i]); - } + jqLiteDealoc(element, true); element.innerHTML = value; }, empty: jqLiteEmpty -}, function(fn, name){ +}, function(fn, name) { /** * Properties: writes return selection, reads return first value */ JQLite.prototype[name] = function(arg1, arg2) { var i, key; + var nodeCount = this.length; // jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it // in a way that survives minification. @@ -2708,7 +2933,7 @@ forEach({ if (isObject(arg1)) { // we are a write, but the object properties are the key/values - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { if (fn === jqLiteData) { // data() takes the whole object in jQuery fn(this[i], arg1); @@ -2722,9 +2947,10 @@ forEach({ return this; } else { // we are a read, so read the first child. + // 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(this.length, 1) : this.length; + var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount; for (var j = 0; j < jj; j++) { var nodeValue = fn(this[j], arg1, arg2); value = value ? value + nodeValue : nodeValue; @@ -2733,7 +2959,7 @@ forEach({ } } else { // we are a write, so apply to all children - for (i = 0; i < this.length; i++) { + for (i = 0; i < nodeCount; i++) { fn(this[i], arg1, arg2); } // return self for chaining @@ -2743,57 +2969,50 @@ forEach({ }); function createEventHandler(element, events) { - var eventHandler = function (event, type) { - if (!event.preventDefault) { - event.preventDefault = function() { - event.returnValue = false; //ie - }; - } + var eventHandler = function(event, type) { + // jQuery specific api + event.isDefaultPrevented = function() { + return event.defaultPrevented; + }; - if (!event.stopPropagation) { - event.stopPropagation = function() { - event.cancelBubble = true; //ie - }; - } + var eventFns = events[type || event.type]; + var eventFnsLength = eventFns ? eventFns.length : 0; - if (!event.target) { - event.target = event.srcElement || document; - } + if (!eventFnsLength) return; + + if (isUndefined(event.immediatePropagationStopped)) { + var originalStopImmediatePropagation = event.stopImmediatePropagation; + event.stopImmediatePropagation = function() { + event.immediatePropagationStopped = true; + + if (event.stopPropagation) { + event.stopPropagation(); + } - if (isUndefined(event.defaultPrevented)) { - var prevent = event.preventDefault; - event.preventDefault = function() { - event.defaultPrevented = true; - prevent.call(event); + if (originalStopImmediatePropagation) { + originalStopImmediatePropagation.call(event); + } }; - event.defaultPrevented = false; } - event.isDefaultPrevented = function() { - return event.defaultPrevented || event.returnValue === false; + event.isImmediatePropagationStopped = function() { + return event.immediatePropagationStopped === true; }; // Copy event handlers in case event handlers array is modified during execution. - var eventHandlersCopy = shallowCopy(events[type || event.type] || []); - - forEach(eventHandlersCopy, function(fn) { - fn.call(element, event); - }); + if ((eventFnsLength > 1)) { + eventFns = shallowCopy(eventFns); + } - // Remove monkey-patched methods (IE), - // as they would cause memory leaks in IE8. - if (msie <= 8) { - // IE7/8 does not allow to delete property on native object - event.preventDefault = null; - event.stopPropagation = null; - event.isDefaultPrevented = null; - } else { - // It shouldn't affect normal browsers (native methods are defined on prototype). - delete event.preventDefault; - delete event.stopPropagation; - delete event.isDefaultPrevented; + for (var i = 0; i < eventFnsLength; i++) { + if (!event.isImmediatePropagationStopped()) { + eventFns[i].call(element, event); + } } }; + + // TODO: this is a hack for angularMocks/clearDataCache that makes it possible to deregister all + // events on `element` eventHandler.elem = element; return eventHandler; } @@ -2806,68 +3025,56 @@ function createEventHandler(element, events) { forEach({ removeData: jqLiteRemoveData, - dealoc: jqLiteDealoc, - - on: function onFn(element, type, fn, unsupported){ + on: function jqLiteOn(element, type, fn, unsupported) { if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); - var events = jqLiteExpandoStore(element, 'events'), - handle = jqLiteExpandoStore(element, 'handle'); + // Do not add event handlers to non-elements because they will not be cleaned up. + if (!jqLiteAcceptsData(element)) { + return; + } + + var expandoStore = jqLiteExpandoStore(element, true); + var events = expandoStore.events; + var handle = expandoStore.handle; + + if (!handle) { + handle = expandoStore.handle = createEventHandler(element, events); + } - if (!events) jqLiteExpandoStore(element, 'events', events = {}); - if (!handle) jqLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); + // http://jsperf.com/string-indexof-vs-split + var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type]; + var i = types.length; - forEach(type.split(' '), function(type){ + while (i--) { + type = types[i]; var eventFns = events[type]; if (!eventFns) { - if (type == 'mouseenter' || type == 'mouseleave') { - var contains = document.body.contains || document.body.compareDocumentPosition ? - function( a, b ) { - // jshint bitwise: false - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - events[type] = []; + 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 - var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; - onFn(element, eventmap[type], function(event) { + 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 && !contains(target, related)) ){ + if (!related || (related !== target && !target.contains(related))) { handle(event, type); } }); } else { - addEventListenerFn(element, type, handle); - events[type] = []; + if (type !== '$destroy') { + addEventListenerFn(element, type, handle); + } } eventFns = events[type]; } eventFns.push(fn); - }); + } }, off: jqLiteOff, @@ -2888,7 +3095,7 @@ forEach({ replaceWith: function(element, replaceNode) { var index, parent = element.parentNode; jqLiteDealoc(element); - forEach(new JQLite(replaceNode), function(node){ + forEach(new JQLite(replaceNode), function(node) { if (index) { parent.insertBefore(node, index.nextSibling); } else { @@ -2900,8 +3107,8 @@ forEach({ children: function(element) { var children = []; - forEach(element.childNodes, function(element){ - if (element.nodeType === 1) + forEach(element.childNodes, function(element) { + if (element.nodeType === NODE_TYPE_ELEMENT) children.push(element); }); return children; @@ -2912,24 +3119,28 @@ forEach({ }, append: function(element, node) { - forEach(new JQLite(node), function(child){ - if (element.nodeType === 1 || element.nodeType === 11) { - element.appendChild(child); - } - }); + var nodeType = element.nodeType; + if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return; + + node = new JQLite(node); + + for (var i = 0, ii = node.length; i < ii; i++) { + var child = node[i]; + element.appendChild(child); + } }, prepend: function(element, node) { - if (element.nodeType === 1) { + if (element.nodeType === NODE_TYPE_ELEMENT) { var index = element.firstChild; - forEach(new JQLite(node), function(child){ + forEach(new JQLite(node), function(child) { element.insertBefore(child, index); }); } }, wrap: function(element, wrapNode) { - wrapNode = jqLite(wrapNode)[0]; + wrapNode = jqLite(wrapNode).eq(0).clone()[0]; var parent = element.parentNode; if (parent) { parent.replaceChild(wrapNode, element); @@ -2937,18 +3148,21 @@ forEach({ wrapNode.appendChild(element); }, - remove: function(element) { - jqLiteDealoc(element); - var parent = element.parentNode; - if (parent) parent.removeChild(element); + remove: jqLiteRemove, + + detach: function(element) { + jqLiteRemove(element, true); }, after: function(element, newElement) { var index = element, parent = element.parentNode; - forEach(new JQLite(newElement), function(node){ + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; parent.insertBefore(node, index.nextSibling); index = node; - }); + } }, addClass: jqLiteAddClass, @@ -2956,7 +3170,7 @@ forEach({ toggleClass: function(element, selector, condition) { if (selector) { - forEach(selector.split(' '), function(className){ + forEach(selector.split(' '), function(className) { var classCondition = condition; if (isUndefined(classCondition)) { classCondition = !jqLiteHasClass(element, className); @@ -2968,20 +3182,11 @@ forEach({ parent: function(element) { var parent = element.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; + return parent && parent.nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT ? parent : null; }, next: function(element) { - if (element.nextElementSibling) { - return element.nextElementSibling; - } - - // IE8 doesn't have nextElementSibling - var elm = element.nextSibling; - while (elm != null && elm.nodeType !== 1) { - elm = elm.nextSibling; - } - return elm; + return element.nextElementSibling; }, find: function(element, selector) { @@ -2994,27 +3199,50 @@ forEach({ clone: jqLiteClone, - triggerHandler: function(element, eventName, eventData) { - var eventFns = (jqLiteExpandoStore(element, 'events') || {})[eventName]; + triggerHandler: function(element, event, extraParameters) { + + var dummyEvent, eventFnsCopy, handlerArgs; + var eventName = event.type || event; + var expandoStore = jqLiteExpandoStore(element); + var events = expandoStore && expandoStore.events; + var eventFns = events && events[eventName]; + + if (eventFns) { + // Create a dummy event to pass to the handlers + dummyEvent = { + preventDefault: function() { this.defaultPrevented = true; }, + isDefaultPrevented: function() { return this.defaultPrevented === true; }, + stopImmediatePropagation: function() { this.immediatePropagationStopped = true; }, + isImmediatePropagationStopped: function() { return this.immediatePropagationStopped === true; }, + stopPropagation: noop, + type: eventName, + target: element + }; - eventData = eventData || []; + // If a custom event was provided then extend our dummy event with it + if (event.type) { + dummyEvent = extend(dummyEvent, event); + } - var event = [{ - preventDefault: noop, - stopPropagation: noop - }]; + // Copy event handlers in case event handlers array is modified during execution. + eventFnsCopy = shallowCopy(eventFns); + handlerArgs = extraParameters ? [dummyEvent].concat(extraParameters) : [dummyEvent]; - forEach(eventFns, function(fn) { - fn.apply(element, event.concat(eventData)); - }); + forEach(eventFnsCopy, function(fn) { + if (!dummyEvent.isImmediatePropagationStopped()) { + fn.apply(element, handlerArgs); + } + }); + } } -}, function(fn, name){ +}, function(fn, name) { /** * chaining functions */ JQLite.prototype[name] = function(arg1, arg2, arg3) { var value; - for(var i=0; i < this.length; i++) { + + for (var i = 0, ii = this.length; i < ii; i++) { if (isUndefined(value)) { value = fn(this[i], arg1, arg2, arg3); if (isDefined(value)) { @@ -3033,6 +3261,27 @@ forEach({ JQLite.prototype.unbind = JQLite.prototype.off; }); + +// Provider for private $$jqLite service +function $$jqLiteProvider() { + this.$get = function $$jqLite() { + return extend(JQLite, { + hasClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteHasClass(node, classes); + }, + addClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteAddClass(node, classes); + }, + removeClass: function(node, classes) { + if (node.attr) node = node[0]; + return jqLiteRemoveClass(node, classes); + } + }); + }; +} + /** * Computes a hash of an 'obj'. * Hash of a: @@ -3045,28 +3294,36 @@ forEach({ * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ -function hashKey(obj) { - var objType = typeof obj, - key; +function hashKey(obj, nextUidFn) { + var key = obj && obj.$$hashKey; - if (objType == 'object' && obj !== null) { - if (typeof (key = obj.$$hashKey) == 'function') { - // must invoke on object to keep the right this + if (key) { + if (typeof key === 'function') { key = obj.$$hashKey(); - } else if (key === undefined) { - key = obj.$$hashKey = nextUid(); } + return key; + } + + var objType = typeof obj; + if (objType == 'function' || (objType == 'object' && obj !== null)) { + key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { - key = obj; + key = objType + ':' + obj; } - return objType + ':' + key; + return key; } /** * HashMap which can use objects as keys */ -function HashMap(array){ +function HashMap(array, isolatedUid) { + if (isolatedUid) { + var uid = 0; + this.nextUid = function() { + return ++uid; + }; + } forEach(array, this.put, this); } HashMap.prototype = { @@ -3076,7 +3333,7 @@ HashMap.prototype = { * @param value value to store can be any type */ put: function(key, value) { - this[hashKey(key)] = value; + this[hashKey(key, this.nextUid)] = value; }, /** @@ -3084,7 +3341,7 @@ HashMap.prototype = { * @returns {Object} the value for the key */ get: function(key) { - return this[hashKey(key)]; + return this[hashKey(key, this.nextUid)]; }, /** @@ -3092,7 +3349,7 @@ HashMap.prototype = { * @param key */ remove: function(key) { - var value = this[key = hashKey(key)]; + var value = this[key = hashKey(key, this.nextUid)]; delete this[key]; return value; } @@ -3102,16 +3359,17 @@ HashMap.prototype = { * @ngdoc function * @module ng * @name angular.injector - * @function + * @kind function * * @description - * Creates an injector function that can be used for retrieving services as well as for + * Creates an injector object that can be used for retrieving services as well as for * dependency injection (see {@link guide/di dependency injection}). * - * @param {Array.<string|Function>} modules A list of module functions or their aliases. See - * {@link angular.module}. The `ng` module must be explicitly added. - * @returns {function()} Injector function. See {@link auto.$injector $injector}. + * {@link angular.module}. The `ng` module must be explicitly added. + * @param {boolean=} [strictDi=false] Whether the injector should be in strict mode, which + * disallows argument name annotation inference. + * @returns {injector} Injector object. See {@link auto.$injector $injector}. * * @example * Typical usage @@ -3121,7 +3379,7 @@ HashMap.prototype = { * * // use the injector to kick off your application * // use the type inference to auto inject arguments, or use implicit injection - * $injector.invoke(function($rootScope, $compile, $document){ + * $injector.invoke(function($rootScope, $compile, $document) { * $compile($document)($rootScope); * $rootScope.$digest(); * }); @@ -3129,7 +3387,7 @@ HashMap.prototype = { * * Sometimes you want to get access to the injector of a currently running Angular app * from outside Angular. Perhaps, you want to inject and compile some markup after the - * application has been bootstrapped. You can do this using extra `injector()` added + * application has been bootstrapped. You can do this using the extra `injector()` added * to JQuery/jqLite elements. See {@link angular.element}. * * *This is fairly rare but could be the case if a third party library is injecting the @@ -3164,20 +3422,39 @@ var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); -function annotate(fn) { + +function anonFn(fn) { + // For anonymous functions, showing at the very least the function signature can help in + // debugging. + var fnText = fn.toString().replace(STRIP_COMMENTS, ''), + args = fnText.match(FN_ARGS); + if (args) { + return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; + } + return 'fn'; +} + +function annotate(fn, strictDi, name) { var $inject, fnText, argDecl, last; - if (typeof fn == 'function') { + if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { + if (strictDi) { + if (!isString(name) || !name) { + name = fn.name || anonFn(fn); + } + throw $injectorMinErr('strictdi', + '{0} is not using explicit annotation and cannot be invoked in strict mode', name); + } fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); - forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ - arg.replace(FN_ARG, function(all, underscore, name){ + forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { + arg.replace(FN_ARG, function(all, underscore, name) { $inject.push(name); }); }); @@ -3199,7 +3476,6 @@ function annotate(fn) { /** * @ngdoc service * @name $injector - * @function * * @description * @@ -3212,9 +3488,9 @@ function annotate(fn) { * ```js * var $injector = angular.injector(); * expect($injector.get('$injector')).toBe($injector); - * expect($injector.invoke(function($injector){ + * expect($injector.invoke(function($injector) { * return $injector; - * }).toBe($injector); + * })).toBe($injector); * ``` * * # Injection Function Annotation @@ -3238,11 +3514,13 @@ function annotate(fn) { * ## Inference * * In JavaScript calling `toString()` on a function returns the function definition. The definition - * can then be parsed and the function arguments can be extracted. *NOTE:* This does not work with - * minification, and obfuscation tools since these tools change the argument names. + * can then be parsed and the function arguments can be extracted. This method of discovering + * annotations is disallowed when the injector is in strict mode. + * *NOTE:* This does not work with minification, and obfuscation tools since these tools change the + * argument names. * * ## `$inject` Annotation - * By adding a `$inject` property onto a function the injection parameters can be specified. + * By adding an `$inject` property onto a function the injection parameters can be specified. * * ## Inline * As an array of injection names, where the last item in the array is the function to call. @@ -3256,6 +3534,7 @@ function annotate(fn) { * Return an instance of the service. * * @param {string} name The name of the instance to retrieve. + * @param {string} caller An optional string to provide the origin of the function call for error messages. * @return {*} The instance. */ @@ -3279,18 +3558,18 @@ function annotate(fn) { * @name $injector#has * * @description - * Allows the user to query if the particular service exist. + * Allows the user to query if the particular service exists. * - * @param {string} Name of the service to query. - * @returns {boolean} returns true if injector has given service. + * @param {string} name Name of the service to query. + * @returns {boolean} `true` if injector has given service. */ /** * @ngdoc method * @name $injector#instantiate * @description - * Create a new instance of JS type. The method takes a constructor function invokes the new - * operator and supplies all of the arguments to the constructor function as specified by the + * Create a new instance of JS type. The method takes a constructor function, invokes the new + * operator, and supplies all of the arguments to the constructor function as specified by the * constructor annotation. * * @param {Function} Type Annotated constructor function. @@ -3324,6 +3603,8 @@ function annotate(fn) { * expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); * ``` * + * You can disallow this method by using strict injection mode. + * * This method does not work with code minification / obfuscation. For this reason the following * annotation strategies are supported. * @@ -3376,6 +3657,8 @@ function annotate(fn) { * @param {Function|Array.<string|Function>} fn Function for which dependent service names need to * be retrieved as described above. * + * @param {boolean=} [strictDi=false] Disallow argument name annotation inference. + * * @returns {Array.<string>} The names of the services which the function requires. */ @@ -3383,7 +3666,7 @@ function annotate(fn) { /** - * @ngdoc object + * @ngdoc service * @name $provide * * @description @@ -3685,11 +3968,12 @@ function annotate(fn) { */ -function createInjector(modulesToLoad) { +function createInjector(modulesToLoad, strictDi) { + strictDi = (strictDi === true); var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap(), + loadedModules = new HashMap([], true), providerCache = { $provide: { provider: supportObject(provider), @@ -3701,14 +3985,17 @@ function createInjector(modulesToLoad) { } }, providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function() { + createInternalInjector(providerCache, function(serviceName, caller) { + if (angular.isString(caller)) { + path.push(caller); + } throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); })), instanceCache = {}, instanceInjector = (instanceCache.$injector = - createInternalInjector(instanceCache, function(servicename) { - var provider = providerInjector.get(servicename + providerSuffix); - return instanceInjector.invoke(provider.$get, provider); + createInternalInjector(instanceCache, function(serviceName, caller) { + var provider = providerInjector.get(serviceName + providerSuffix, caller); + return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); })); @@ -3741,7 +4028,21 @@ function createInjector(modulesToLoad) { return providerCache[name + providerSuffix] = provider_; } - function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } + function enforceReturnValue(name, factory) { + return function enforcedReturnValue() { + var result = instanceInjector.invoke(factory, this); + if (isUndefined(result)) { + throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); + } + return result; + }; + } + + function factory(name, factoryFn, enforce) { + return provider(name, { + $get: enforce !== false ? enforceReturnValue(name, factoryFn) : factoryFn + }); + } function service(name, constructor) { return factory(name, ['$injector', function($injector) { @@ -3749,7 +4050,7 @@ function createInjector(modulesToLoad) { }]); } - function value(name, val) { return factory(name, valueFn(val)); } + function value(name, val) { return factory(name, valueFn(val), false); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); @@ -3770,23 +4071,28 @@ function createInjector(modulesToLoad) { //////////////////////////////////// // Module Loading //////////////////////////////////// - function loadModules(modulesToLoad){ - var runBlocks = [], moduleFn, invokeQueue, i, ii; + function loadModules(modulesToLoad) { + var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; loadedModules.put(module, true); + function runInvokeQueue(queue) { + var i, ii; + for (i = 0, ii = queue.length; i < ii; i++) { + var invokeArgs = queue[i], + provider = providerInjector.get(invokeArgs[0]); + + provider[invokeArgs[1]].apply(provider, invokeArgs[2]); + } + } + try { if (isString(module)) { moduleFn = angularModule(module); runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); - - for(invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { - var invokeArgs = invokeQueue[i], - provider = providerInjector.get(invokeArgs[0]); - - provider[invokeArgs[1]].apply(provider, invokeArgs[2]); - } + runInvokeQueue(moduleFn._invokeQueue); + runInvokeQueue(moduleFn._configBlocks); } else if (isFunction(module)) { runBlocks.push(providerInjector.invoke(module)); } else if (isArray(module)) { @@ -3819,17 +4125,18 @@ function createInjector(modulesToLoad) { function createInternalInjector(cache, factory) { - function getService(serviceName) { + function getService(serviceName, caller) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', + serviceName + ' <- ' + path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName); + return cache[serviceName] = factory(serviceName, caller); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -3841,13 +4148,18 @@ function createInjector(modulesToLoad) { } } - function invoke(fn, self, locals){ + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; + } + var args = [], - $inject = annotate(fn), + $inject = annotate(fn, strictDi, serviceName), length, i, key; - for(i = 0, length = $inject.length; i < length; i++) { + for (i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', @@ -3856,11 +4168,10 @@ function createInjector(modulesToLoad) { args.push( locals && locals.hasOwnProperty(key) ? locals[key] - : getService(key) + : getService(key, serviceName) ); } - if (!fn.$inject) { - // this means that we must be an array. + if (isArray(fn)) { fn = fn[length]; } @@ -3869,15 +4180,12 @@ function createInjector(modulesToLoad) { return fn.apply(self, args); } - function instantiate(Type, locals) { - var Constructor = function() {}, - instance, returnedValue; - + function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; - instance = new Constructor(); - returnedValue = invoke(Type, instance, locals); + // Object creation: http://jsperf.com/create-constructor/2 + var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype); + var returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; } @@ -3894,100 +4202,267 @@ function createInjector(modulesToLoad) { } } +createInjector.$$annotate = annotate; + /** - * @ngdoc service - * @name $anchorScroll - * @kind function - * @requires $window - * @requires $location - * @requires $rootScope + * @ngdoc provider + * @name $anchorScrollProvider * * @description - * When called, it checks current value of `$location.hash()` and scroll to related element, - * according to rules specified in - * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document). - * - * It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. - * This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. - * - * @example - <example> - <file name="index.html"> - <div id="scrollArea" ng-controller="ScrollCtrl"> - <a ng-click="gotoBottom()">Go to bottom</a> - <a id="bottom"></a> You're at the bottom! - </div> - </file> - <file name="script.js"> - function ScrollCtrl($scope, $location, $anchorScroll) { - $scope.gotoBottom = function (){ - // set the location.hash to the id of - // the element you wish to scroll to. - $location.hash('bottom'); - - // call $anchorScroll() - $anchorScroll(); - }; - } - </file> - <file name="style.css"> - #scrollArea { - height: 350px; - overflow: auto; - } - - #bottom { - display: block; - margin-top: 2000px; - } - </file> - </example> + * Use `$anchorScrollProvider` to disable automatic scrolling whenever + * {@link ng.$location#hash $location.hash()} changes. */ function $AnchorScrollProvider() { var autoScrollingEnabled = true; + /** + * @ngdoc method + * @name $anchorScrollProvider#disableAutoScrolling + * + * @description + * By default, {@link ng.$anchorScroll $anchorScroll()} will automatically detect changes to + * {@link ng.$location#hash $location.hash()} and scroll to the element matching the new hash.<br /> + * Use this method to disable automatic scrolling. + * + * If automatic scrolling is disabled, one must explicitly call + * {@link ng.$anchorScroll $anchorScroll()} in order to scroll to the element related to the + * current hash. + */ this.disableAutoScrolling = function() { autoScrollingEnabled = false; }; + /** + * @ngdoc service + * @name $anchorScroll + * @kind function + * @requires $window + * @requires $location + * @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). + * + * 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 + * {@link ng.$anchorScrollProvider#disableAutoScrolling $anchorScrollProvider.disableAutoScrolling()}. + * + * Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a + * vertical scroll-offset (either fixed or dynamic). + * + * @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. + * + * `yOffset` can be specified in various ways: + * - **number**: A fixed number of pixels to be used as offset.<br /><br /> + * - **function**: A getter function called everytime `$anchorScroll()` is executed. Must return + * a number representing the offset (in pixels).<br /><br /> + * - **jqLite**: A jqLite/jQuery element to be used for specifying the offset. The distance from + * the top of the page to the element's bottom will be used as offset.<br /> + * **Note**: The element will be taken into account only as long as its `position` is set to + * `fixed`. This option is useful, when dealing with responsive navbars/headers that adjust + * their height and/or positioning according to the viewport's size. + * + * <br /> + * <div class="alert alert-warning"> + * In order for `yOffset` to work properly, scrolling should take place on the document's root and + * not some child element. + * </div> + * + * @example + <example module="anchorScrollExample"> + <file name="index.html"> + <div id="scrollArea" ng-controller="ScrollController"> + <a ng-click="gotoBottom()">Go to bottom</a> + <a id="bottom"></a> You're at the bottom! + </div> + </file> + <file name="script.js"> + angular.module('anchorScrollExample', []) + .controller('ScrollController', ['$scope', '$location', '$anchorScroll', + function ($scope, $location, $anchorScroll) { + $scope.gotoBottom = function() { + // set the location.hash to the id of + // the element you wish to scroll to. + $location.hash('bottom'); + + // call $anchorScroll() + $anchorScroll(); + }; + }]); + </file> + <file name="style.css"> + #scrollArea { + height: 280px; + overflow: auto; + } + + #bottom { + display: block; + margin-top: 2000px; + } + </file> + </example> + * + * <hr /> + * The example below illustrates the use of a vertical scroll-offset (specified as a fixed value). + * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. + * + * @example + <example module="anchorScrollOffsetExample"> + <file name="index.html"> + <div class="fixed-header" ng-controller="headerCtrl"> + <a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]"> + Go to anchor {{x}} + </a> + </div> + <div id="anchor{{x}}" class="anchor" ng-repeat="x in [1,2,3,4,5]"> + Anchor {{x}} of 5 + </div> + </file> + <file name="script.js"> + angular.module('anchorScrollOffsetExample', []) + .run(['$anchorScroll', function($anchorScroll) { + $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels + }]) + .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', + function ($anchorScroll, $location, $scope) { + $scope.gotoAnchor = function(x) { + var newHash = 'anchor' + x; + if ($location.hash() !== newHash) { + // set the $location.hash to `newHash` and + // $anchorScroll will automatically scroll to it + $location.hash('anchor' + x); + } else { + // call $anchorScroll() explicitly, + // since $location.hash hasn't changed + $anchorScroll(); + } + }; + } + ]); + </file> + <file name="style.css"> + body { + padding-top: 50px; + } + + .anchor { + border: 2px dashed DarkOrchid; + padding: 10px 10px 200px 10px; + } + + .fixed-header { + background-color: rgba(0, 0, 0, 0.2); + height: 50px; + position: fixed; + top: 0; left: 0; right: 0; + } + + .fixed-header > a { + display: inline-block; + margin: 5px 15px; + } + </file> + </example> + */ this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { var document = $window.document; - // helper function to get first anchor from a NodeList - // can't use filter.filter, as it accepts only instances of Array - // and IE can't convert NodeList to an array using [].slice - // TODO(vojta): use filter if we change it to accept lists as well + // Helper function to get first anchor from a NodeList + // (using `Array#some()` instead of `angular#forEach()` since it's more performant + // and working in all supported browsers.) function getFirstAnchor(list) { var result = null; - forEach(list, function(element) { - if (!result && lowercase(element.nodeName) === 'a') result = element; + Array.prototype.some.call(list, function(element) { + if (nodeName_(element) === 'a') { + result = element; + return true; + } }); return result; } + function getYOffset() { + + var offset = scroll.yOffset; + + if (isFunction(offset)) { + offset = offset(); + } else if (isElement(offset)) { + var elem = offset[0]; + var style = $window.getComputedStyle(elem); + if (style.position !== 'fixed') { + offset = 0; + } else { + offset = elem.getBoundingClientRect().bottom; + } + } else if (!isNumber(offset)) { + offset = 0; + } + + return offset; + } + + function scrollTo(elem) { + if (elem) { + elem.scrollIntoView(); + + var offset = getYOffset(); + + if (offset) { + // `offset` is the number of pixels we should scroll UP in order to align `elem` properly. + // This is true ONLY if the call to `elem.scrollIntoView()` initially aligns `elem` at the + // top of the viewport. + // + // IF the number of pixels from the top of `elem` to the end of the page's content is less + // than the height of the viewport, then `elem.scrollIntoView()` will align the `elem` some + // way down the page. + // + // This is often the case for elements near the bottom of the page. + // + // In such cases we do not need to scroll the whole `offset` up, just the difference between + // the top of the element and the offset, which is enough to align the top of `elem` at the + // desired position. + var elemTop = elem.getBoundingClientRect().top; + $window.scrollBy(0, elemTop - offset); + } + } else { + $window.scrollTo(0, 0); + } + } + function scroll() { var hash = $location.hash(), elm; // empty hash, scroll to the top of the page - if (!hash) $window.scrollTo(0, 0); + if (!hash) scrollTo(null); // element with given id - else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); + else if ((elm = document.getElementById(hash))) scrollTo(elm); // first anchor with given name :-D - else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); + else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); // no element and hash == 'top', scroll to the top of the page - else if (hash === 'top') $window.scrollTo(0, 0); + else if (hash === 'top') scrollTo(null); } // does not scroll when user clicks on anchor link that is currently on // (no url change, no $location.hash() change), browser native does scroll if (autoScrollingEnabled) { $rootScope.$watch(function autoScrollWatch() {return $location.hash();}, - function autoScrollWatchAction() { - $rootScope.$evalAsync(scroll); + function autoScrollWatchAction(newVal, oldVal) { + // skip the initial scroll if $location.hash is empty + if (newVal === oldVal && newVal === '') return; + + jqLiteDocumentLoaded(function() { + $rootScope.$evalAsync(scroll); + }); }); } @@ -4070,16 +4545,81 @@ var $AnimateProvider = ['$provide', function($provide) { * @return {RegExp} The current CSS className expression value. If null then there is no expression value */ this.classNameFilter = function(expression) { - if(arguments.length === 1) { + if (arguments.length === 1) { this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; } return this.$$classNameFilter; }; - this.$get = ['$timeout', '$$asyncCallback', function($timeout, $$asyncCallback) { + this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) { + + var currentDefer; - function async(fn) { - fn && $$asyncCallback(fn); + function runAnimationPostDigest(fn) { + var cancelFn, defer = $$q.defer(); + defer.promise.$$cancelFn = function ngAnimateMaybeCancel() { + cancelFn && cancelFn(); + }; + + $rootScope.$$postDigest(function ngAnimatePostDigest() { + cancelFn = fn(function ngAnimateNotifyComplete() { + defer.resolve(); + }); + }); + + return defer.promise; + } + + function resolveElementClasses(element, classes) { + var toAdd = [], toRemove = []; + + var hasClasses = createMap(); + forEach((element.attr('class') || '').split(/\s+/), function(className) { + hasClasses[className] = true; + }); + + forEach(classes, function(status, className) { + var hasClass = hasClasses[className]; + + // If the most recent class manipulation (via $animate) was to remove the class, and the + // element currently has the class, the class is scheduled for removal. Otherwise, if + // the most recent class manipulation (via $animate) was to add the class, and the + // element does not currently have the class, the class is scheduled to be added. + if (status === false && hasClass) { + toRemove.push(className); + } else if (status === true && !hasClass) { + toAdd.push(className); + } + }); + + return (toAdd.length + toRemove.length) > 0 && + [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null]; + } + + function cachedClassManipulation(cache, classes, op) { + for (var i=0, ii = classes.length; i < ii; ++i) { + var className = classes[i]; + cache[className] = op; + } + } + + function asyncPromise() { + // only serve one instance of a promise in order to save CPU cycles + if (!currentDefer) { + currentDefer = $$q.defer(); + $$asyncCallback(function() { + currentDefer.resolve(); + currentDefer = null; + }); + } + return currentDefer.promise; + } + + function applyStyles(element, options) { + if (angular.isObject(options)) { + var styles = extend(options.from || {}, options.to || {}); + element.css(styles); + } } /** @@ -4100,58 +4640,58 @@ var $AnimateProvider = ['$provide', function($provide) { * page}. */ return { + animate: function(element, from, to) { + applyStyles(element, { from: from, to: to }); + return asyncPromise(); + }, /** * * @ngdoc method * @name $animate#enter - * @function - * @description Inserts the element into the DOM either after the `after` element or within - * the `parent` element. Once complete, the done() callback will be fired (if provided). + * @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. * @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 {Function=} done callback function that will be called after the element has been - * inserted into the DOM + * @param {object=} options an optional collection of styles that will be applied to the element. + * @return {Promise} the animation callback promise */ - enter : function(element, parent, after, done) { - if (after) { - after.after(element); - } else { - if (!parent || !parent[0]) { - parent = after.parent(); - } - parent.append(element); - } - async(done); + enter: function(element, parent, after, options) { + applyStyles(element, options); + after ? after.after(element) + : parent.prepend(element); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#leave - * @function - * @description Removes the element from the DOM. Once complete, the done() callback will be - * fired (if provided). + * @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 {Function=} done callback function that will be called after the element has been - * removed from the DOM + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - leave : function(element, done) { + leave: function(element, options) { element.remove(); - async(done); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#move - * @function + * @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. Once complete, the - * done() callback will be fired (if provided). + * 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. * * @param {DOMElement} element the element which will be moved around within the * DOM @@ -4159,89 +4699,148 @@ var $AnimateProvider = ['$provide', function($provide) { * inserted into (if the after element is not present) * @param {DOMElement} after the sibling element where the element will be * positioned next to - * @param {Function=} done the callback function (if provided) that will be fired after the - * element has been moved to its new position + * @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, done) { + 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. - this.enter(element, parent, after, done); + return this.enter(element, parent, after, options); }, /** * * @ngdoc method * @name $animate#addClass - * @function - * @description Adds the provided className CSS class value to the provided element. Once - * complete, the done() callback will be fired (if provided). + * @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 {Function=} done the callback function (if provided) that will be fired after the - * className value has been added to the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - addClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { + 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); }); - async(done); + applyStyles(element, options); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#removeClass - * @function + * @kind function * @description Removes the provided className CSS class value from the provided element. - * Once complete, the done() callback will be fired (if provided). + * 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 {Function=} done the callback function (if provided) that will be fired after the - * className value has been removed from the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - removeClass : function(element, className, done) { - className = isString(className) ? - className : - isArray(className) ? className.join(' ') : ''; - forEach(element, function (element) { + 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); }); - async(done); + applyStyles(element, options); + return asyncPromise(); }, /** * * @ngdoc method * @name $animate#setClass - * @function + * @kind function * @description Adds and/or removes the given CSS classes to and from the element. - * Once complete, the done() callback will be fired (if provided). - * @param {DOMElement} element the element which will it's CSS classes changed + * 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 {Function=} done the callback function (if provided) that will be fired after the - * CSS classes have been set on the element + * @param {object=} options an optional collection of options that will be applied to the element. + * @return {Promise} the animation callback promise */ - setClass : function(element, add, remove, done) { - forEach(element, function (element) { - jqLiteAddClass(element, add); - jqLiteRemoveClass(element, remove); - }); - async(done); + setClass: function(element, add, remove, options) { + var self = this; + var STORAGE_KEY = '$$animateClasses'; + var createdCache = false; + element = jqLite(element); + + var cache = element.data(STORAGE_KEY); + if (!cache) { + cache = { + classes: {}, + options: options + }; + createdCache = true; + } else if (options && cache.options) { + cache.options = angular.extend(cache.options || {}, options); + } + + var classes = cache.classes; + + add = isArray(add) ? add : add.split(' '); + remove = isArray(remove) ? remove : remove.split(' '); + cachedClassManipulation(classes, add, true); + cachedClassManipulation(classes, remove, false); + + if (createdCache) { + cache.promise = runAnimationPostDigest(function(done) { + var cache = element.data(STORAGE_KEY); + element.removeData(STORAGE_KEY); + + // in the event that the element is removed before postDigest + // is run then the cache will be undefined and there will be + // no need anymore to add or remove and of the element classes + if (cache) { + var classes = resolveElementClasses(element, cache.classes); + if (classes) { + self.$$setClassImmediately(element, classes[0], classes[1], cache.options); + } + } + + done(); + }); + element.data(STORAGE_KEY, cache); + } + + return cache.promise; + }, + + $$setClassImmediately: function(element, add, remove, options) { + add && this.$$addClassImmediately(element, add); + remove && this.$$removeClassImmediately(element, remove); + applyStyles(element, options); + return asyncPromise(); }, - enabled : noop + enabled: noop, + cancel: noop }; }]; }]; -function $$AsyncCallbackProvider(){ +function $$AsyncCallbackProvider() { this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) { return $$rAF.supported ? function(fn) { return $$rAF(fn); } @@ -4251,6 +4850,8 @@ function $$AsyncCallbackProvider(){ }]; } +/* global stripHash: true */ + /** * ! This is a private undocumented service ! * @@ -4269,8 +4870,7 @@ function $$AsyncCallbackProvider(){ /** * @param {object} window The global window object. * @param {object} document jQuery wrapped document. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. + * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer) { @@ -4301,7 +4901,7 @@ function Browser(window, document, $log, $sniffer) { } finally { outstandingRequestCount--; if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { + while (outstandingRequestCallbacks.length) { try { outstandingRequestCallbacks.pop()(); } catch (e) { @@ -4312,6 +4912,11 @@ function Browser(window, document, $log, $sniffer) { } } + function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index + 1); + } + /** * @private * Note: this method is used only by scenario runner @@ -4322,7 +4927,7 @@ function Browser(window, document, $log, $sniffer) { // 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(); }); + forEach(pollFns, function(pollFn) { pollFn(); }); if (outstandingRequestCount === 0) { callback(); @@ -4364,7 +4969,7 @@ function Browser(window, document, $log, $sniffer) { */ function startPoller(interval, setTimeout) { (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); + forEach(pollFns, function(pollFn) { pollFn(); }); pollTimeout = setTimeout(check, interval); })(); } @@ -4373,9 +4978,13 @@ function Browser(window, document, $log, $sniffer) { // URL API ////////////////////////////////////////////////////////////// - var lastBrowserUrl = location.href, + var cachedState, lastHistoryState, + lastBrowserUrl = location.href, baseElement = document.find('base'), - newLocation = null; + reloadLocation = null; + + cacheState(); + lastHistoryState = cachedState; /** * @name $browser#url @@ -4394,52 +5003,110 @@ function Browser(window, document, $log, $sniffer) { * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? + * @param {boolean=} replace Should new url replace current history record? + * @param {object=} state object to use with pushState/replaceState */ - self.url = function(url, replace) { + self.url = function(url, replace, state) { + // In modern browsers `history.state` is `null` by default; treating it separately + // from `undefined` would cause `$browser.url('/foo')` to change `history.state` + // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. + if (isUndefined(state)) { + state = null; + } + // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { - if (lastBrowserUrl == url) return; + var sameState = lastHistoryState === state; + + // Don't change anything if previous and current URLs and states match. This also prevents + // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. + // See https://github.com/angular/angular.js/commit/ffb2701 + if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { + return self; + } + var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else { - history.pushState(null, '', url); - // Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 - baseElement.attr('href', baseElement.attr('href')); - } + lastHistoryState = state; + // Don't use history API if only the hash changed + // due to a bug in IE10/IE11 which leads + // to not firing a `hashchange` nor `popstate` event + // in some cases (see #9143). + if ($sniffer.history && (!sameBase || !sameState)) { + history[replace ? 'replaceState' : 'pushState'](state, '', url); + cacheState(); + // Do the assignment again so that those two variables are referentially identical. + lastHistoryState = cachedState; } else { - newLocation = url; + if (!sameBase) { + reloadLocation = url; + } if (replace) { location.replace(url); - } else { + } else if (!sameBase) { location.href = url; + } else { + location.hash = getHash(url); } } return self; // getter } else { - // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href - // methods not updating location.href synchronously. + // - reloadLocation is needed as browsers don't allow to read out + // the new location.href if a reload happened. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + return reloadLocation || location.href.replace(/%27/g,"'"); } }; + /** + * @name $browser#state + * + * @description + * This method is a getter. + * + * Return history.state or null if history.state is undefined. + * + * @returns {object} state + */ + self.state = function() { + return cachedState; + }; + var urlChangeListeners = [], urlChangeInit = false; + function cacheStateAndFireUrlChange() { + cacheState(); + fireUrlChange(); + } + + // This variable should be used *only* inside the cacheState function. + var lastCachedState = null; + function cacheState() { + // This should be the only place in $browser where `history.state` is read. + cachedState = window.history.state; + cachedState = isUndefined(cachedState) ? null : cachedState; + + // Prevent callbacks fo fire twice if both hashchange & popstate were fired. + if (equals(cachedState, lastCachedState)) { + cachedState = lastCachedState; + } + lastCachedState = cachedState; + } + function fireUrlChange() { - newLocation = null; - if (lastBrowserUrl == self.url()) return; + if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { + return; + } lastBrowserUrl = self.url(); + lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { - listener(self.url()); + listener(self.url(), cachedState); }); } @@ -4472,11 +5139,9 @@ function Browser(window, document, $log, $sniffer) { // changed by push/replaceState // html5 history api - popstate event - if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); + if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event - if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); + jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } @@ -4485,6 +5150,13 @@ function Browser(window, document, $log, $sniffer) { return callback; }; + /** + * 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. + */ + self.$$checkUrlChange = fireUrlChange; + ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// @@ -4510,6 +5182,14 @@ function Browser(window, document, $log, $sniffer) { var lastCookieString = ''; var cookiePath = self.baseHref(); + function safeDecodeURIComponent(str) { + try { + return decodeURIComponent(str); + } catch (e) { + return str; + } + } + /** * @name $browser#cookies * @@ -4531,16 +5211,15 @@ function Browser(window, document, $log, $sniffer) { * @returns {Object} Hash of all cookies (if called without any parameter) */ self.cookies = function(name, value) { - /* global escape: false, unescape: false */ var cookieLength, cookieArray, cookie, i, index; if (name) { if (value === undefined) { - rawDocument.cookie = escape(name) + "=;path=" + cookiePath + + rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; } else { if (isString(value)) { - cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(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: @@ -4548,8 +5227,8 @@ function Browser(window, document, $log, $sniffer) { // - 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 ("+ + $log.warn("Cookie '" + name + + "' possibly not set or overflowed because it was too large (" + cookieLength + " > 4096 bytes)!"); } } @@ -4564,12 +5243,12 @@ function Browser(window, document, $log, $sniffer) { cookie = cookieArray[i]; index = cookie.indexOf('='); if (index > 0) { //ignore nameless cookies - name = unescape(cookie.substring(0, index)); + 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] = unescape(cookie.substring(index + 1)); + lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1)); } } } @@ -4627,9 +5306,9 @@ function Browser(window, document, $log, $sniffer) { } -function $BrowserProvider(){ +function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ + function($window, $log, $sniffer, $document) { return new Browser($window, $document, $log, $sniffer); }]; } @@ -4701,8 +5380,10 @@ function $BrowserProvider(){ $scope.keys = []; $scope.cache = $cacheFactory('cacheId'); $scope.put = function(key, value) { - $scope.cache.put(key, value); - $scope.keys.push(key); + if ($scope.cache.get(key) === undefined) { + $scope.keys.push(key); + } + $scope.cache.put(key, value === undefined ? null : value); }; }]); </file> @@ -4775,7 +5456,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#put - * @function + * @kind function * * @description * Inserts a named entry into the {@link $cacheFactory.Cache Cache} object to be @@ -4811,7 +5492,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#get - * @function + * @kind function * * @description * Retrieves named data stored in the {@link $cacheFactory.Cache Cache} object. @@ -4835,7 +5516,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#remove - * @function + * @kind function * * @description * Removes an entry from the {@link $cacheFactory.Cache Cache} object. @@ -4863,7 +5544,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#removeAll - * @function + * @kind function * * @description * Clears the cache object of any entries. @@ -4879,7 +5560,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#destroy - * @function + * @kind function * * @description * Destroys the {@link $cacheFactory.Cache Cache} object entirely, @@ -4896,7 +5577,7 @@ function $CacheFactoryProvider() { /** * @ngdoc method * @name $cacheFactory.Cache#info - * @function + * @kind function * * @description * Retrieve information regarding a particular {@link $cacheFactory.Cache Cache}. @@ -4951,7 +5632,7 @@ function $CacheFactoryProvider() { * @name $cacheFactory#info * * @description - * Get information about all the of the caches that have been created + * Get information about all the caches that have been created * * @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` */ @@ -5001,7 +5682,8 @@ function $CacheFactoryProvider() { * ``` * * **Note:** the `script` tag containing the template does not need to be included in the `head` of - * the document, but it must be below the `ng-app` definition. + * the document, but it must be a descendent of the {@link ng.$rootElement $rootElement} (IE, + * element with ng-app attribute), otherwise the template will be ignored. * * Adding via the $templateCache service: * @@ -5052,7 +5734,7 @@ function $TemplateCacheProvider() { /** * @ngdoc service * @name $compile - * @function + * @kind function * * @description * Compiles an HTML string or DOM into a template and produces a template function, which @@ -5090,9 +5772,9 @@ function $TemplateCacheProvider() { * template: '<div></div>', // or // function(tElement, tAttrs) { ... }, * // or * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * replace: false, * transclude: false, * restrict: 'A', + * templateNamespace: 'html', * scope: false, * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, * controllerAs: 'stringAlias', @@ -5143,6 +5825,13 @@ function $TemplateCacheProvider() { * The directive definition object provides instructions to the {@link ng.$compile * compiler}. The attributes are: * + * #### `multiElement` + * When this property is set to true, the HTML compiler will collect DOM nodes between + * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them + * together as the directive elements. It is recommended that this feature be used on directives + * which are not strictly behavioural (such as {@link ngClick}), and which + * do not manipulate or replace child nodes (such as {@link ngInclude}). + * * #### `priority` * When there are multiple directives defined on a single DOM element, sometimes it * is necessary to specify the order in which the directives are applied. The `priority` is used @@ -5154,7 +5843,8 @@ function $TemplateCacheProvider() { * #### `terminal` * If set to true then the current `priority` will be the last set of directives * which will execute (any directives at the current priority will still execute - * as the order of execution on same `priority` is undefined). + * as the order of execution on same `priority` is undefined). Note that expressions + * 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 @@ -5187,19 +5877,25 @@ function $TemplateCacheProvider() { * 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. + * 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 and to the parent scope, this can be + * 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})`. * * + * #### `bindToController` + * When an isolate scope is used for a component (see above), and `controllerAs` is used, `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. * * #### `controller` * Controller constructor function. The controller is instantiated before the @@ -5210,9 +5906,18 @@ function $TemplateCacheProvider() { * * `$scope` - Current scope associated with the element * * `$element` - Current element * * `$attrs` - Current attributes object for the element - * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. - * `function([scope], cloneLinkingFn)`. + * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: + * `function([scope], cloneLinkingFn, futureParentElement)`. + * * `scope`: optional argument to override the scope. + * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. + * * `futureParentElement`: + * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. + * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. + * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) + * and when the `cloneLinkinFn` is passed, + * as those elements need to created and cloned in a special way when they are defined outside their + * usual containers (e.g. like `<svg>`). + * * See also the `directive.templateNamespace` property. * * * #### `require` @@ -5223,9 +5928,12 @@ function $TemplateCacheProvider() { * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. - * * `^` - Locate the required controller by searching the element's parents. Throw an error if not found. - * * `?^` - Attempt to locate the required controller by searching the element's parents or pass `null` to the - * `link` fn if not found. + * * `^` - Locate the required controller by searching the element and its parents. Throw an error if not found. + * * `^^` - Locate the required controller by searching the element's parents. Throw an error if not found. + * * `?^` - Attempt to locate the required controller by searching the element and its parents or pass + * `null` to the `link` fn if not found. + * * `?^^` - Attempt to locate the required controller by searching the element's parents, or pass + * `null` to the `link` fn if not found. * * * #### `controllerAs` @@ -5236,54 +5944,84 @@ function $TemplateCacheProvider() { * * #### `restrict` * String of subset of `EACM` which restricts the directive to a specific directive - * declaration style. If omitted, the default (attributes only) is used. + * declaration style. If omitted, the defaults (elements and attributes) are used. * - * * `E` - Element name: `<my-directive></my-directive>` + * * `E` - Element name (default): `<my-directive></my-directive>` * * `A` - Attribute (default): `<div my-directive="exp"></div>` * * `C` - Class: `<div class="my-directive: exp;"></div>` * * `M` - Comment: `<!-- directive: my-directive exp -->` * * + * #### `templateNamespace` + * String representing the document type used by the markup in the template. + * AngularJS needs this information as those elements need to be created and cloned + * in a special way when they are defined outside their usual containers like `<svg>` and `<math>`. + * + * * `html` - All root nodes in the template are HTML. Root nodes may also be + * top-level elements such as `<svg>` or `<math>`. + * * `svg` - The root nodes in the template are SVG elements (excluding `<math>`). + * * `math` - The root nodes in the template are MathML elements (excluding `<svg>`). + * + * If no `templateNamespace` is specified, then the namespace is considered to be `html`. + * * #### `template` - * replace the current element with the contents of the HTML. The replacement process - * migrates all of the attributes / classes from the old element to the new one. See the - * {@link guide/directive#creating-custom-directives_creating-directives_template-expanding-directive - * Directives Guide} for an example. + * HTML markup that may: + * * Replace the contents of the directive's element (default). + * * Replace the directive's element itself (if `replace` is true - DEPRECATED). + * * Wrap the contents of the directive's element (if `transclude` is true). * - * You can specify `template` as a string representing the template or as a function which takes - * two arguments `tElement` and `tAttrs` (described in the `compile` function api below) and - * returns a string value representing the template. + * Value may be: + * + * * A string. For example `<div red-on-hover>{{delete_str}}</div>`. + * * A function which takes two arguments `tElement` and `tAttrs` (described in the `compile` + * function api below) and returns a string value. * * * #### `templateUrl` - * Same as `template` but the template is loaded from the specified URL. Because - * the template loading is asynchronous the compilation/linking is suspended until the template - * is loaded. + * This is similar to `template` but the template is loaded from the specified URL, asynchronously. + * + * Because template loading is asynchronous the compiler will suspend compilation of directives on that element + * for later when the template has been resolved. In the meantime it will continue to compile and link + * sibling and parent elements as though this element had not contained any directives. + * + * The compiler does not suspend the entire compilation to wait for templates to be loaded because this + * would result in the whole app "stalling" until all templates are loaded asynchronously - even in the + * case when only one deeply nested directive has `templateUrl`. + * + * Template loading is asynchronous even if the template has been preloaded into the {@link $templateCache} * * You can specify `templateUrl` as a string representing the URL or as a function which takes two * arguments `tElement` and `tAttrs` (described in the `compile` function api below) and returns * a string value representing the url. In either case, the template URL is passed through {@link - * api/ng.$sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * $sce#getTrustedResourceUrl $sce.getTrustedResourceUrl}. + * * + * #### `replace` ([*DEPRECATED*!], will be removed in next major release - i.e. v2.0) + * specify what the template should replace. Defaults to `false`. * - * #### `replace` - * specify where the template should be inserted. Defaults to `false`. + * * `true` - the template will replace the directive's element. + * * `false` - the template will replace the contents of the directive's element. * - * * `true` - the template will replace the current element. - * * `false` - the template will replace the contents of the current element. + * The replacement process migrates all of the attributes / classes from the old element to the new + * one. See the {@link guide/directive#template-expanding-directive + * Directives Guide} for an example. * + * There are very few scenarios where element replacement is required for the application function, + * the main one being reusable custom components that are used within SVG contexts + * (because SVG doesn't work with custom elements in the DOM tree). * * #### `transclude` - * compile the content of the element and make it available to the directive. - * Typically used with {@link ng.directive:ngTransclude - * ngTransclude}. The advantage of transclusion is that the linking function receives a - * transclusion function which is pre-bound to the correct scope. In a typical setup the widget - * creates an `isolate` scope, but the transclusion is not a child, but a sibling of the `isolate` - * scope. This makes it possible for the widget to have private state, and the transclusion to - * be bound to the parent (pre-`isolate`) scope. + * Extract the contents of the element where the directive appears and make it available to the directive. + * The contents are compiled and provided to the directive as a **transclusion function**. See the + * {@link $compile#transclusion Transclusion} section below. + * + * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element or the entire element: * - * * `true` - transclude the content of the directive. - * * `'element'` - transclude the whole element including any directives defined at lower priority. + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. * * * #### `compile` @@ -5293,11 +6031,7 @@ function $TemplateCacheProvider() { * ``` * * The compile function deals with transforming the template DOM. Since most directives do not do - * template transformation, it is not used often. Examples that require compile functions are - * directives that transform template DOM, such as {@link - * api/ng.directive:ngRepeat ngRepeat}, or load the contents - * asynchronously, such as {@link ngRoute.directive:ngView ngView}. The - * compile function takes the following arguments. + * template transformation, it is not used often. The compile function takes the following arguments: * * * `tElement` - template element - The element where the directive has been declared. It is * safe to do template transformation on the element and child elements only. @@ -5366,10 +6100,9 @@ function $TemplateCacheProvider() { * the directives to use the controllers as a communication channel. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * The scope can be overridden by an optional first argument. This is the same as the `$transclude` - * parameter of directive controllers. - * `function([scope], cloneLinkingFn)`. - * + * This is the same as the `$transclude` + * parameter of directive controllers, see there for details. + * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function * @@ -5378,9 +6111,130 @@ function $TemplateCacheProvider() { * * #### Post-linking function * - * Executed after the child elements are linked. It is safe to do DOM transformation in the post-linking function. + * Executed after the child elements are linked. + * + * Note that child elements that contain `templateUrl` directives will not have been compiled + * and linked since they are waiting for their template to load asynchronously and their own + * compilation and linking has been suspended until that occurs. + * + * It is safe to do DOM transformation in the post-linking function on elements that are not waiting + * for their async templates to be resolved. + * + * + * ### Transclusion + * + * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and + * copying them to another part of the DOM, while maintaining their connection to the original AngularJS + * scope from where they were taken. + * + * Transclusion is used (often with {@link ngTransclude}) to insert the + * original contents of a directive's element into a specified place in the template of the directive. + * The benefit of transclusion, over simply moving the DOM elements manually, is that the transcluded + * content has access to the properties on the scope from which it was taken, even if the directive + * has isolated scope. + * See the {@link guide/directive#creating-a-directive-that-wraps-other-elements Directives Guide}. + * + * This makes it possible for the widget to have private state for its template, while the transcluded + * content has access to its originating scope. + * + * <div class="alert alert-warning"> + * **Note:** When testing an element transclude directive you must not place the directive at the root of the + * DOM fragment that is being compiled. See {@link guide/unit-testing#testing-transclusion-directives + * Testing Transclusion Directives}. + * </div> + * + * #### Transclusion Functions + * + * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion + * function** to the directive's `link` function and `controller`. This transclusion function is a special + * **linking function** that will return the compiled contents linked to a new transclusion scope. + * + * <div class="alert alert-info"> + * If you are just using {@link ngTransclude} then you don't need to worry about this function, since + * ngTransclude will deal with it for us. + * </div> + * + * If you want to manually control the insertion and removal of the transcluded content in your directive + * then you must use this transclude function. When you call a transclude function it returns a a jqLite/JQuery + * object that contains the compiled DOM, which is linked to the correct transclusion scope. + * + * When you call a transclusion function you can pass in a **clone attach function**. This function accepts + * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded + * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * + * <div class="alert alert-info"> + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function + * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. + * </div> + * + * It is normal practice to attach your transcluded content (`clone`) to the DOM inside your **clone + * attach function**: + * + * ```js + * var transcludedContent, transclusionScope; + * + * $transclude(function(clone, scope) { + * element.append(clone); + * transcludedContent = clone; + * transclusionScope = scope; + * }); + * ``` + * + * Later, if you want to remove the transcluded content from your DOM then you should also destroy the + * associated transclusion scope: + * + * ```js + * transcludedContent.remove(); + * transclusionScope.$destroy(); + * ``` + * + * <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), + * then you are also responsible for calling `$destroy` on the transclusion scope. + * </div> + * + * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} + * automatically destroy their transluded clones as necessary so you do not need to worry about this if + * you are simply using {@link ngTransclude} to inject the transclusion into your directive. + * + * + * #### Transclusion Scopes + * + * When you call a transclude function it returns a DOM fragment that is pre-bound to a **transclusion + * scope**. This scope is special, in that it is a child of the directive's scope (and so gets destroyed + * when the directive's scope gets destroyed) but it inherits the properties of the scope from which it + * was taken. + * + * For example consider a directive that uses transclusion and isolated scope. The DOM hierarchy might look + * like this: + * + * ```html + * <div ng-app> + * <div isolate> + * <div transclusion> + * </div> + * </div> + * </div> + * ``` + * + * The `$parent` scope hierarchy will look like this: + * + * ``` + * - $rootScope + * - isolate + * - transclusion + * ``` + * + * but the scopes will inherit prototypically from different scopes to their `$parent`. + * + * ``` + * - $rootScope + * - transclusion + * - isolate + * ``` + * * - * <a name="Attributes"></a> * ### Attributes * * The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the @@ -5418,17 +6272,17 @@ function $TemplateCacheProvider() { * } * ``` * - * Below is an example using `$compileProvider`. + * ## Example * * <div class="alert alert-warning"> * **Note**: Typically directives are registered with `module.directive`. The example below is * to illustrate how `$compile` works. * </div> * - <example module="compile"> + <example module="compileExample"> <file name="index.html"> <script> - angular.module('compile', [], function($compileProvider) { + angular.module('compileExample', [], function($compileProvider) { // configure new 'compile' directive by passing a directive // factory function. The factory function injects the '$compile' $compileProvider.directive('compile', function($compile) { @@ -5452,15 +6306,14 @@ function $TemplateCacheProvider() { } ); }; - }) - }); - - function Ctrl($scope) { + }); + }) + .controller('GreeterController', ['$scope', function($scope) { $scope.name = 'Angular'; $scope.html = 'Hello {{name}}'; - } + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="GreeterController"> <input ng-model="name"> <br> <textarea ng-model="html"></textarea> <br> <div compile="html"></div> @@ -5482,10 +6335,17 @@ 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. + * @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED. + * + * <div class="alert alert-error"> + * **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. + * </div> + * * @param {number} maxPriority only apply directives lower than given priority (Only effects the * root element(s), not their children) - * @returns {function(scope, cloneAttachFn=)} a link function which is used to bind template + * @returns {function(scope, cloneAttachFn=, options=)} a link function which is used to bind template * (a DOM element/tree) to a scope. Where: * * * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. @@ -5497,6 +6357,19 @@ function $TemplateCacheProvider() { * * `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. * + * * `options` - An optional object hash with linking options. If `options` is provided, then the following + * keys may be used to control linking behavior: + * + * * `parentBoundTranscludeFn` - the transclude function made available to + * 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. + * * `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. + * * Calling the linking function returns the element of the template. It is either the original * element passed in, or the clone of the element if the `cloneAttachFn` is provided. * @@ -5535,7 +6408,6 @@ var $compileMinErr = minErr('$compile'); /** * @ngdoc provider * @name $compileProvider - * @function * * @description */ @@ -5543,18 +6415,46 @@ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/; + COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/, + ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), + REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // 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)$/; + function parseIsolateBindings(scope, directiveName) { + var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; + + var bindings = {}; + + forEach(scope, function(definition, scopeName) { + var match = definition.match(LOCAL_REGEXP); + + if (!match) { + throw $compileMinErr('iscp', + "Invalid isolate scope definition for directive '{0}'." + + " Definition: {... {1}: '{2}' ...}", + directiveName, scopeName, definition); + } + + bindings[scopeName] = { + mode: match[1][0], + collection: match[2] === '*', + optional: match[3] === '?', + attrName: match[4] || scopeName + }; + }); + + return bindings; + } + /** * @ngdoc method * @name $compileProvider#directive - * @function + * @kind function * * @description * Register a new directive with the compiler. @@ -5587,7 +6487,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.index = index; directive.name = directive.name || name; directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'A'; + directive.restrict = directive.restrict || 'EA'; + if (isObject(directive.scope)) { + directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name); + } directives.push(directive); } catch (e) { $exceptionHandler(e); @@ -5607,13 +6510,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compileProvider#aHrefSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe * urls during a[href] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at preventing XSS attacks via html links. * * Any url about to be assigned to a[href] via data-binding is first normalized and turned into * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` @@ -5637,7 +6540,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compileProvider#imgSrcSanitizationWhitelist - * @function + * @kind function * * @description * Retrieves or overrides the default regular expression that is used for whitelisting of safe @@ -5663,25 +6566,82 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + /** + * @ngdoc method + * @name $compileProvider#debugInfoEnabled + * + * @param {boolean=} enabled update the debugInfoEnabled state if provided, otherwise just return the + * current debugInfoEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable various debug runtime information in the compiler such as adding + * binding information and a reference to the current scope on to DOM elements. + * If enabled, the compiler will add the following to DOM elements that have been bound to the scope + * * `ng-binding` CSS class + * * `$binding` data property containing an array of the binding expressions + * + * You may want to disable this in production for a significant performance boost. See + * {@link guide/production#disabling-debug-data Disabling Debug Data} for more. + * + * The default value is true. + */ + var debugInfoEnabled = true; + this.debugInfoEnabled = function(enabled) { + if (isDefined(enabled)) { + debugInfoEnabled = enabled; + return this; + } + return debugInfoEnabled; + }; + this.$get = [ - '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', + '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', - function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, + function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { - var Attributes = function(element, attr) { + var Attributes = function(element, attributesToCopy) { + if (attributesToCopy) { + var keys = Object.keys(attributesToCopy); + var i, l, key; + + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + this[key] = attributesToCopy[key]; + } + } else { + this.$attr = {}; + } + this.$$element = element; - this.$attr = attr || {}; }; Attributes.prototype = { + /** + * @ngdoc method + * @name $compile.directive.Attributes#$normalize + * @kind function + * + * @description + * Converts an attribute name (e.g. dash/colon/underscore-delimited string, optionally prefixed with `x-` or + * `data-`) to its normalized, camelCase form. + * + * Also there is special case for Moz prefix starting with upper case letter. + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * @param {string} name Name to normalize + */ $normalize: directiveNormalize, /** * @ngdoc method * @name $compile.directive.Attributes#$addClass - * @function + * @kind function * * @description * Adds the CSS class value specified by the classVal parameter to the element. If animations @@ -5689,8 +6649,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * @param {string} classVal The className value that will be added to the element */ - $addClass : function(classVal) { - if(classVal && classVal.length > 0) { + $addClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.addClass(this.$$element, classVal); } }, @@ -5698,7 +6658,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$removeClass - * @function + * @kind function * * @description * Removes the CSS class value specified by the classVal parameter from the element. If @@ -5706,8 +6666,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * * @param {string} classVal The className value that will be removed from the element */ - $removeClass : function(classVal) { - if(classVal && classVal.length > 0) { + $removeClass: function(classVal) { + if (classVal && classVal.length > 0) { $animate.removeClass(this.$$element, classVal); } }, @@ -5715,7 +6675,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$updateClass - * @function + * @kind function * * @description * Adds and removes the appropriate CSS class values to the element based on the difference @@ -5724,16 +6684,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} newClasses The current CSS className value * @param {string} oldClasses The former CSS className value */ - $updateClass : function(newClasses, oldClasses) { + $updateClass: function(newClasses, oldClasses) { var toAdd = tokenDifference(newClasses, oldClasses); - var toRemove = tokenDifference(oldClasses, newClasses); + if (toAdd && toAdd.length) { + $animate.addClass(this.$$element, toAdd); + } - if(toAdd.length === 0) { + var toRemove = tokenDifference(oldClasses, newClasses); + if (toRemove && toRemove.length) { $animate.removeClass(this.$$element, toRemove); - } else if(toRemove.length === 0) { - $animate.addClass(this.$$element, toAdd); - } else { - $animate.setClass(this.$$element, toAdd, toRemove); } }, @@ -5751,13 +6710,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //is set through this function since it may cause $updateClass to //become unstable. - var booleanKey = getBooleanAttrName(this.$$element[0], key), - normalizedVal, + var node = this.$$element[0], + booleanKey = getBooleanAttrName(node, key), + aliasedKey = getAliasedAttrName(node, key), + observer = key, nodeName; if (booleanKey) { this.$$element.prop(key, value); attrName = booleanKey; + } else if (aliasedKey) { + this[aliasedKey] = value; + observer = aliasedKey; } this[key] = value; @@ -5774,10 +6738,44 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - // sanitize a[href] and img[src] values - if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')) { + if ((nodeName === 'a' && key === 'href') || + (nodeName === 'img' && key === 'src')) { + // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); + } else if (nodeName === 'img' && key === 'srcset') { + // sanitize img[srcset] values + var result = ""; + + // first check if there are spaces because it's not the same pattern + var trimmedSrcset = trim(value); + // ( 999x ,| 999w ,| ,|, ) + var srcPattern = /(\s+\d+x\s*,|\s+\d+w\s*,|\s+,|,\s+)/; + var pattern = /\s/.test(trimmedSrcset) ? srcPattern : /(,)/; + + // split srcset into tuple of uri and descriptor except for the last item + var rawUris = trimmedSrcset.split(pattern); + + // for each tuples + var nbrUrisWith2parts = Math.floor(rawUris.length / 2); + for (var i = 0; i < nbrUrisWith2parts; i++) { + var innerIdx = i * 2; + // sanitize the uri + result += $$sanitizeUri(trim(rawUris[innerIdx]), true); + // add the descriptor + result += (" " + trim(rawUris[innerIdx + 1])); + } + + // split the last item into uri and descriptor + var lastTuple = trim(rawUris[i * 2]).split(/\s/); + + // sanitize the last uri + result += $$sanitizeUri(trim(lastTuple[0]), true); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (" " + trim(lastTuple[1])); + } + this[key] = value = result; } if (writeAttr !== false) { @@ -5790,7 +6788,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[key], function(fn) { + $$observers && forEach($$observers[observer], function(fn) { try { fn(value); } catch (e) { @@ -5803,7 +6801,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** * @ngdoc method * @name $compile.directive.Attributes#$observe - * @function + * @kind function * * @description * Observes an interpolated attribute. @@ -5815,25 +6813,39 @@ 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#Attributes Directives} guide for more info. - * @returns {function()} the `fn` parameter. + * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. + * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { var attrs = this, - $$observers = (attrs.$$observers || (attrs.$$observers = {})), + $$observers = (attrs.$$observers || (attrs.$$observers = createMap())), listeners = ($$observers[key] || ($$observers[key] = [])); listeners.push(fn); $rootScope.$evalAsync(function() { - if (!listeners.$$inter) { + if (!listeners.$$inter && attrs.hasOwnProperty(key)) { // no one registered attribute interpolation function, so lets call it manually fn(attrs[key]); } }); - return fn; + + return function() { + arrayRemove(listeners, fn); + }; } }; + + function safeAddClass($element, className) { + try { + $element.addClass(className); + } catch (e) { + // ignore, since it means that we are trying to set class on + // SVG element, where class name is read-only. + } + } + + var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') @@ -5843,6 +6855,30 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }, NG_ATTR_BINDING = /^ngAttr[A-Z]/; + compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { + var bindings = $element.data('$binding') || []; + + if (isArray(binding)) { + bindings = bindings.concat(binding); + } else { + bindings.push(binding); + } + + $element.data('$binding', bindings); + } : noop; + + compile.$$addBindingClass = debugInfoEnabled ? function $$addBindingClass($element) { + safeAddClass($element, 'ng-binding'); + } : noop; + + compile.$$addScopeInfo = debugInfoEnabled ? function $$addScopeInfo($element, scope, isolated, noTemplate) { + var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope'; + $element.data(dataName, scope); + } : noop; + + compile.$$addScopeClass = debugInfoEnabled ? function $$addScopeClass($element, isolated) { + safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); + } : noop; return compile; @@ -5857,48 +6893,74 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } // 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 == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; + 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]; } }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); - safeAddClass($compileNodes, 'ng-scope'); - return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){ + compile.$$addScopeClass($compileNodes); + var namespace = null; + return function publicLinkFn(scope, cloneConnectFn, options) { assertArg(scope, 'scope'); - // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart - // and sometimes changes the structure of the DOM. - var $linkNode = cloneConnectFn - ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! - : $compileNodes; - - forEach(transcludeControllers, function(instance, name) { - $linkNode.data('$' + name + 'Controller', instance); - }); - // Attach scope only to non-text nodes. - for(var i = 0, ii = $linkNode.length; i<ii; i++) { - var node = $linkNode[i], - nodeType = node.nodeType; - if (nodeType === 1 /* element */ || nodeType === 9 /* document */) { - $linkNode.eq(i).data('$scope', scope); + options = options || {}; + var parentBoundTranscludeFn = options.parentBoundTranscludeFn, + transcludeControllers = options.transcludeControllers, + futureParentElement = options.futureParentElement; + + // When `parentBoundTranscludeFn` is passed, it is a + // `controllersBoundTransclude` function (it was previously passed + // as `transclude` to directive.link) so we must unwrap it to get + // its `boundTranscludeFn` + if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) { + parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude; + } + + if (!namespace) { + namespace = detectNamespaceForChildElements(futureParentElement); + } + var $linkNode; + if (namespace !== 'html') { + // When using a directive with replace:true and templateUrl the $compileNodes + // (or a child element inside of them) + // might change, so we need to recreate the namespace adapted compileNodes + // for call to the link function. + // Note: This will already clone the nodes... + $linkNode = jqLite( + wrapTemplate(namespace, jqLite('<div>').append($compileNodes).html()) + ); + } else if (cloneConnectFn) { + // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart + // and sometimes changes the structure of the DOM. + $linkNode = JQLitePrototype.clone.call($compileNodes); + } else { + $linkNode = $compileNodes; + } + + if (transcludeControllers) { + for (var controllerName in transcludeControllers) { + $linkNode.data('$' + controllerName + 'Controller', transcludeControllers[controllerName].instance); } } + compile.$$addScopeInfo($linkNode, scope); + if (cloneConnectFn) cloneConnectFn($linkNode, scope); - if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); + if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode; }; } - function safeAddClass($element, className) { - try { - $element.addClass(className); - } catch(e) { - // ignore, since it means that we are trying to set class on - // SVG element, where class name is read-only. + function detectNamespaceForChildElements(parentElement) { + // TODO: Make this detect MathML as well... + var node = parentElement && parentElement[0]; + if (!node) { + return 'html'; + } else { + return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; } } @@ -5920,7 +6982,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], - attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound; + attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); @@ -5935,7 +6997,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : null; if (nodeLinkFn && nodeLinkFn.scope) { - safeAddClass(jqLite(nodeList[i]), 'ng-scope'); + compile.$$addScopeClass(attrs.$$element); } childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || @@ -5943,10 +7005,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { !childNodes.length) ? null : compileNodes(childNodes, - nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); + nodeLinkFn ? ( + (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement) + && nodeLinkFn.transclude) : transcludeFn); + + if (nodeLinkFn || childLinkFn) { + linkFns.push(i, nodeLinkFn, childLinkFn); + linkFnFound = true; + nodeLinkFnFound = nodeLinkFnFound || nodeLinkFn; + } - linkFns.push(nodeLinkFn, childLinkFn); - linkFnFound = linkFnFound || nodeLinkFn || childLinkFn; //use the previous context only for the first element in the virtual group previousCompileContext = null; } @@ -5954,60 +7022,80 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // return a linking function if we have found anything, null otherwise return linkFnFound ? compositeLinkFn : null; - function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { - var nodeLinkFn, childLinkFn, node, $node, childScope, childTranscludeFn, i, ii, n; + function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn) { + var nodeLinkFn, childLinkFn, node, childScope, i, ii, idx, childBoundTranscludeFn; + var stableNodeList; - // copy nodeList so that linking doesn't break due to live list updates. - var nodeListLength = nodeList.length, - stableNodeList = new Array(nodeListLength); - for (i = 0; i < nodeListLength; i++) { - stableNodeList[i] = nodeList[i]; + + if (nodeLinkFnFound) { + // copy nodeList so that if a nodeLinkFn removes or adds an element at this DOM level our + // offsets don't get screwed up + var nodeListLength = nodeList.length; + stableNodeList = new Array(nodeListLength); + + // create a sparse array by only copying the elements which have a linkFn + for (i = 0; i < linkFns.length; i+=3) { + idx = linkFns[i]; + stableNodeList[idx] = nodeList[idx]; + } + } else { + stableNodeList = nodeList; } - for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { - node = stableNodeList[n]; + for (i = 0, ii = linkFns.length; i < ii;) { + node = stableNodeList[linkFns[i++]]; nodeLinkFn = linkFns[i++]; childLinkFn = linkFns[i++]; - $node = jqLite(node); if (nodeLinkFn) { if (nodeLinkFn.scope) { childScope = scope.$new(); - $node.data('$scope', childScope); + compile.$$addScopeInfo(jqLite(node), childScope); } else { childScope = scope; } - childTranscludeFn = nodeLinkFn.transclude; - if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, - createBoundTranscludeFn(scope, childTranscludeFn || transcludeFn) - ); + + if (nodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn( + scope, nodeLinkFn.transclude, parentBoundTranscludeFn, + nodeLinkFn.elementTranscludeOnThisElement); + + } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { + childBoundTranscludeFn = parentBoundTranscludeFn; + + } else if (!parentBoundTranscludeFn && transcludeFn) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); + } else { - nodeLinkFn(childLinkFn, childScope, node, $rootElement, boundTranscludeFn); + childBoundTranscludeFn = null; } + + nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); + } else if (childLinkFn) { - childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); + childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); } } } } - function createBoundTranscludeFn(scope, transcludeFn) { - return function boundTranscludeFn(transcludedScope, cloneFn, controllers) { - var scopeCreated = false; + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) { + + var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { - transcludedScope = scope.$new(); + transcludedScope = scope.$new(false, containingScope); transcludedScope.$$transcluded = true; - scopeCreated = true; } - var clone = transcludeFn(transcludedScope, cloneFn, controllers); - if (scopeCreated) { - clone.on('$destroy', bind(transcludedScope, transcludedScope.$destroy)); - } - return clone; + return transcludeFn(transcludedScope, cloneFn, { + parentBoundTranscludeFn: previousBoundTranscludeFn, + transcludeControllers: controllers, + futureParentElement: futureParentElement + }); }; + + return boundTranscludeFn; } /** @@ -6026,44 +7114,51 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { match, className; - switch(nodeType) { - case 1: /* Element */ + switch (nodeType) { + case NODE_TYPE_ELEMENT: /* Element */ // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, + for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; attr = nAttrs[j]; - if (!msie || msie >= 8 || attr.specified) { - name = attr.name; - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (NG_ATTR_BINDING.test(ngAttrName)) { - name = snake_case(ngAttrName.substr(6), '-'); - } + name = attr.name; + value = trim(attr.value); + + // support ngAttr attribute binding + ngAttrName = directiveNormalize(name); + if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + name = name.replace(PREFIX_REGEXP, '') + .substr(8).replace(/_(.)/g, function(match, letter) { + return letter.toUpperCase(); + }); + } - var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); + 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); } + } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - attrs[nName] = value = trim(attr.value); - if (getBooleanAttrName(node, nName)) { - attrs[nName] = true; // presence means true - } - addAttrInterpolateDirective(node, directives, value, nName); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); + nName = directiveNormalize(name.toLowerCase()); + attrsMap[nName] = name; + if (isNgAttr || !attrs.hasOwnProperty(nName)) { + attrs[nName] = value; + if (getBooleanAttrName(node, nName)) { + attrs[nName] = true; // presence means true + } } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); + addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, + attrEndName); } // use class as directive @@ -6078,10 +7173,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } break; - case 3: /* Text Node */ + case NODE_TYPE_TEXT: /* Text Node */ addTextInterpolateDirective(directives, node.nodeValue); break; - case 8: /* Comment */ + case NODE_TYPE_COMMENT: /* Comment */ try { match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); if (match) { @@ -6114,14 +7209,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodes = []; var depth = 0; if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { - var startNode = node; do { if (!node) { throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); } - if (node.nodeType == 1 /** Element **/) { + if (node.nodeType == NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -6181,10 +7275,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var terminalPriority = -Number.MAX_VALUE, newScopeDirective, controllerDirectives = previousCompileContext.controllerDirectives, + controllers, newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, templateDirective = previousCompileContext.templateDirective, nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective, hasTranscludeDirective = false, + hasTemplate = false, hasElementTranscludeDirective = previousCompileContext.hasElementTranscludeDirective, $compileNode = templateAttrs.$$element = jqLite(compileNode), directive, @@ -6196,7 +7292,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveValue; // executes all directives on the current element - for(var i = 0, ii = directives.length; i < ii; i++) { + for (var i = 0, ii = directives.length; i < ii; i++) { directive = directives[i]; var attrStart = directive.$$start; var attrEnd = directive.$$end; @@ -6212,17 +7308,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directiveValue = directive.scope) { - newScopeDirective = newScopeDirective || directive; // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives if (!directive.templateUrl) { - assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, - $compileNode); if (isObject(directiveValue)) { + // This directive is trying to add an isolated scope. + // Check that there is no scope of any kind already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective, + directive, $compileNode); newIsolateScopeDirective = directive; + } else { + // This directive is trying to add a child scope. + // Check that there is no isolated scope already + assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, + $compileNode); } } + + newScopeDirective = newScopeDirective || directive; } directiveName = directive.name; @@ -6249,12 +7353,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; - $template = groupScan(compileNode, attrStart, attrEnd); + $template = $compileNode; $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; - replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); + replaceWith(jqCollection, sliceArgs($template), compileNode); childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { @@ -6275,6 +7379,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.template) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -6289,11 +7394,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(directiveValue)) { $template = []; } else { - $template = jqLite(directiveValue); + $template = removeComments(wrapTemplate(directive.templateNamespace, trim(directiveValue))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, ''); @@ -6324,6 +7429,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (directive.templateUrl) { + hasTemplate = true; assertNoDuplicate('template', templateDirective, directive, $compileNode); templateDirective = directive; @@ -6332,7 +7438,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, - templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { + templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, newIsolateScopeDirective: newIsolateScopeDirective, templateDirective: templateDirective, @@ -6360,7 +7466,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true; - nodeLinkFn.transclude = hasTranscludeDirective && childTranscludeFn; + nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective; + nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective; + nodeLinkFn.templateOnThisElement = hasTemplate; + nodeLinkFn.transclude = childTranscludeFn; + previousCompileContext.hasElementTranscludeDirective = hasElementTranscludeDirective; // might be normal or delayed nodeLinkFn depending on if templateUrl is present @@ -6372,6 +7482,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (pre) { if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); pre.require = directive.require; + pre.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { pre = cloneAndAnnotateFn(pre, {isolateScope: true}); } @@ -6380,6 +7491,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (post) { if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); post.require = directive.require; + post.directiveName = directiveName; if (newIsolateScopeDirective === directive || directive.$$isolateScope) { post = cloneAndAnnotateFn(post, {isolateScope: true}); } @@ -6388,33 +7500,47 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - function getControllers(require, $element, elementControllers) { + function getControllers(directiveName, require, $element, elementControllers) { var value, retrievalMethod = 'data', optional = false; + var $searchElement = $element; + var match; if (isString(require)) { - while((value = require.charAt(0)) == '^' || value == '?') { - require = require.substr(1); - if (value == '^') { - retrievalMethod = 'inheritedData'; - } - optional = optional || value == '?'; + match = require.match(REQUIRE_PREFIX_REGEXP); + require = require.substring(match[0].length); + + 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; } + value = null; if (elementControllers && retrievalMethod === 'data') { - value = elementControllers[require]; + if (value = elementControllers[require]) { + value = value.instance; + } } - value = value || $element[retrievalMethod]('$' + require + 'Controller'); + value = value || $searchElement[retrievalMethod]('$' + require + 'Controller'); if (!value && !optional) { throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); } - return value; + return value || null; } else if (isArray(require)) { value = []; forEach(require, function(require) { - value.push(getControllers(require, $element, elementControllers)); + value.push(getControllers(directiveName, require, $element, elementControllers)); }); } return value; @@ -6422,52 +7548,91 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var attrs, $element, i, ii, linkFn, controller, isolateScope, elementControllers = {}, transcludeFn; + var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element, + attrs; if (compileNode === linkNode) { attrs = templateAttrs; + $element = templateAttrs.$$element; } else { - attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); + $element = jqLite(linkNode); + attrs = new Attributes($element, templateAttrs); } - $element = attrs.$$element; if (newIsolateScopeDirective) { - var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; - var $linkNode = jqLite(linkNode); - isolateScope = scope.$new(true); + } - if (templateDirective && (templateDirective === newIsolateScopeDirective.$$originalDirective)) { - $linkNode.data('$isolateScope', isolateScope) ; - } else { - $linkNode.data('$isolateScopeNoTemplate', isolateScope); - } + if (boundTranscludeFn) { + // track `boundTranscludeFn` so it can be unwrapped if `transcludeFn` + // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` + transcludeFn = controllersBoundTransclude; + transcludeFn.$$boundTransclude = boundTranscludeFn; + } + 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]; + } - safeAddClass($linkNode, 'ng-isolate-scope'); + controllerInstance = $controller(controller, locals, true, directive.controllerAs); - forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { - var match = definition.match(LOCAL_REGEXP) || [], - attrName = match[3] || scopeName, - optional = (match[2] == '?'), - mode = match[1], // @, =, or & + // 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; + }); + } + + if (newIsolateScopeDirective) { + 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; + } + + forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) { + var attrName = definition.attrName, + optional = definition.optional, + mode = definition.mode, // @, =, or & lastValue, parentGet, parentSet, compare; - isolateScope.$$isolateBindings[scopeName] = mode + attrName; - switch (mode) { case '@': attrs.$observe(attrName, function(value) { - isolateScope[scopeName] = value; + isolateBindingContext[scopeName] = value; }); attrs.$$observers[attrName].$$scope = scope; - if( attrs[attrName] ) { + 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 - isolateScope[scopeName] = $interpolate(attrs[attrName])(scope); + isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope); } break; @@ -6479,88 +7644,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a,b) { return a === b; }; + 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 = isolateScope[scopeName] = parentGet(scope); + lastValue = isolateBindingContext[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", attrs[attrName], newIsolateScopeDirective.name); }; - lastValue = isolateScope[scopeName] = parentGet(scope); - isolateScope.$watch(function parentValueWatch() { - var parentValue = parentGet(scope); - if (!compare(parentValue, isolateScope[scopeName])) { + 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 - isolateScope[scopeName] = parentValue; + isolateBindingContext[scopeName] = parentValue; } else { // if the parent can be assigned then do so - parentSet(scope, parentValue = isolateScope[scopeName]); + parentSet(scope, parentValue = isolateBindingContext[scopeName]); } } return lastValue = parentValue; - }, null, parentGet.literal); + }; + 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; case '&': parentGet = $parse(attrs[attrName]); - isolateScope[scopeName] = function(locals) { + isolateBindingContext[scopeName] = function(locals) { return parentGet(scope, locals); }; break; - - default: - throw $compileMinErr('iscp', - "Invalid isolate scope definition for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", - newIsolateScopeDirective.name, scopeName, definition); } }); } - transcludeFn = boundTranscludeFn && controllersBoundTransclude; - if (controllerDirectives) { - 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); - // 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); - } - - if (directive.controllerAs) { - locals.$scope[directive.controllerAs] = controllerInstance; - } + if (controllers) { + forEach(controllers, function(controller) { + controller(); }); + controllers = null; } // PRELINKING - for(i = 0, ii = preLinkFns.length; i < ii; i++) { - try { - linkFn = preLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = 0, ii = preLinkFns.length; i < ii; i++) { + linkFn = preLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // RECURSION @@ -6573,22 +7715,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); // POSTLINKING - for(i = postLinkFns.length - 1; i >= 0; i--) { - try { - linkFn = postLinkFns[i]; - linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, - linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn); - } catch (e) { - $exceptionHandler(e, startingTag($element)); - } + for (i = postLinkFns.length - 1; i >= 0; i--) { + linkFn = postLinkFns[i]; + invokeLinkFn(linkFn, + linkFn.isolateScope ? isolateScope : scope, + $element, + attrs, + linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), + transcludeFn + ); } // This is the function that is injected as `$transclude`. - function controllersBoundTransclude(scope, cloneAttachFn) { + // Note: all arguments are optional! + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { var transcludeControllers; - // no scope passed - if (arguments.length < 2) { + // No scope passed in: + if (!isScope(scope)) { + futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; } @@ -6596,8 +7741,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } - - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); + if (!futureParentElement) { + futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; + } + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } @@ -6628,11 +7775,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (name === ignoreDirective) return null; var match = null; if (hasDirectives.hasOwnProperty(name)) { - for(var directive, directives = $injector.get(name + Suffix), - i = 0, ii = directives.length; i<ii; i++) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { try { directive = directives[i]; - if ( (maxPriority === undefined || maxPriority > directive.priority) && + if ((maxPriority === undefined || maxPriority > directive.priority) && directive.restrict.indexOf(location) != -1) { if (startAttrName) { directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); @@ -6640,7 +7787,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { tDirectives.push(directive); match = directive; } - } catch(e) { $exceptionHandler(e); } + } catch (e) { $exceptionHandler(e); } } } return match; @@ -6648,6 +7795,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { /** + * looks up the directive and returns true if it is a multi-element directive, + * and therefore requires DOM nodes between -start and -end markers to be grouped + * together. + * + * @param {string} name name of the directive to look up. + * @returns true if directive was registered as multi-element. + */ + function directiveIsMultiElement(name) { + if (hasDirectives.hasOwnProperty(name)) { + for (var directive, directives = $injector.get(name + Suffix), + i = 0, ii = directives.length; i < ii; i++) { + directive = directives[i]; + if (directive.multiElement) { + return true; + } + } + } + return false; + } + + /** * When the element is replaced with HTML template then the new attributes * on the template need to be merged with the existing attributes in the DOM. * The desired effect is to have both of the attributes present. @@ -6663,7 +7831,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // reapply the old attributes to the new element forEach(dst, function(value, key) { if (key.charAt(0) != '$') { - if (src[key]) { + if (src[key] && src[key] !== value) { value += (key === 'style' ? ';' : ' ') + src[key]; } dst.$set(key, value, true, srcAttr[key]); @@ -6702,12 +7870,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }), templateUrl = (isFunction(origAsyncDirective.templateUrl)) ? origAsyncDirective.templateUrl($compileNode, tAttrs) - : origAsyncDirective.templateUrl; + : origAsyncDirective.templateUrl, + templateNamespace = origAsyncDirective.templateNamespace; $compileNode.empty(); - $http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). - success(function(content) { + $templateRequest($sce.getTrustedResourceUrl(templateUrl)) + .then(function(content) { var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn; content = denormalizeTemplate(content); @@ -6716,11 +7885,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (jqLiteIsTextNode(content)) { $template = []; } else { - $template = jqLite(content); + $template = removeComments(wrapTemplate(templateNamespace, trim(content))); } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== 1) { + if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", origAsyncDirective.name, templateUrl); @@ -6752,14 +7921,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { }); afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); - - while(linkQueue.length) { + while (linkQueue.length) { var scope = linkQueue.shift(), beforeTemplateLinkNode = linkQueue.shift(), linkRootElement = linkQueue.shift(), boundTranscludeFn = linkQueue.shift(), linkNode = $compileNode[0]; + if (scope.$$destroyed) continue; + if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { var oldClasses = beforeTemplateLinkNode.className; @@ -6768,14 +7938,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // it was cloned therefore we have to clone as well. linkNode = jqLiteClone(compileNode); } - replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); // Copy in CSS classes from original node safeAddClass(jqLite(linkNode), oldClasses); } - if (afterTemplateNodeLinkFn.transclude) { - childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); } else { childBoundTranscludeFn = boundTranscludeFn; } @@ -6783,19 +7952,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; - }). - error(function(response, code, headers, config) { - throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { + var childBoundTranscludeFn = boundTranscludeFn; + if (scope.$$destroyed) return; if (linkQueue) { - linkQueue.push(scope); - linkQueue.push(node); - linkQueue.push(rootElement); - linkQueue.push(boundTranscludeFn); + linkQueue.push(scope, + node, + rootElement, + childBoundTranscludeFn); } else { - afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn); + if (afterTemplateNodeLinkFn.transcludeOnThisElement) { + childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn); + } + afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn); } }; } @@ -6825,20 +7996,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (interpolateFn) { directives.push({ priority: 0, - compile: valueFn(function textInterpolateLinkFn(scope, node) { - var parent = node.parent(), - bindings = parent.data('$binding') || []; - bindings.push(interpolateFn); - safeAddClass(parent.data('$binding', bindings), 'ng-binding'); - scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { - node[0].nodeValue = value; - }); - }) + compile: function textInterpolateCompileFn(templateNode) { + var templateNodeParent = templateNode.parent(), + hasCompileParent = !!templateNodeParent.length; + + // When transcluding a template that has bindings in the root + // we don't have a parent and thus need to add the class during linking fn. + if (hasCompileParent) compile.$$addBindingClass(templateNodeParent); + + return function textInterpolateLinkFn(scope, node) { + var parent = node.parent(); + if (!hasCompileParent) compile.$$addBindingClass(parent); + compile.$$addBindingInfo(parent, interpolateFn.expressions); + scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { + node[0].nodeValue = value; + }); + }; + } }); } } + function wrapTemplate(type, template) { + type = lowercase(type || 'html'); + switch (type) { + case 'svg': + case 'math': + var wrapper = document.createElement('div'); + wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>'; + return wrapper.childNodes[0].childNodes; + default: + return template; + } + } + + function getTrustedContext(node, attrNormalizedName) { if (attrNormalizedName == "srcdoc") { return $sce.HTML; @@ -6846,22 +8039,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var tag = nodeName_(node); // maction[xlink:href] can source SVG. It's not limited to <maction>. if (attrNormalizedName == "xlinkHref" || - (tag == "FORM" && attrNormalizedName == "action") || - (tag != "IMG" && (attrNormalizedName == "src" || + (tag == "form" && attrNormalizedName == "action") || + (tag != "img" && (attrNormalizedName == "src" || attrNormalizedName == "ngSrc"))) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name) { - var interpolateFn = $interpolate(value, true); + function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + var trustedContext = getTrustedContext(node, name); + allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + + var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - if (name === "multiple" && nodeName_(node) === "SELECT") { + if (name === "multiple" && nodeName_(node) === "select") { throw $compileMinErr("selmulti", "Binding to the 'multiple' attribute is not supported. Element: {0}", startingTag(node)); @@ -6880,17 +8076,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { "ng- versions (such as ng-click instead of onclick) instead."); } - // we need to interpolate again, in case the attribute value has been updated - // (e.g. by another directive's compile function) - interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); + // If the attribute has changed since last $interpolate()ed + var newValue = attr[name]; + if (newValue !== value) { + // we need to interpolate again since the attribute value has been updated + // (e.g. by another directive's compile function) + // ensure unset/empty values make interpolateFn falsy + interpolateFn = newValue && $interpolate(newValue, true, trustedContext, allOrNothing); + value = newValue; + } // if attribute was updated so that there is no interpolation going on we don't want to // register any observers if (!interpolateFn) return; - // TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the - // actual attr value + // initialize attr object so that it's ready in case we need the value for isolate + // scope initialization, otherwise the value would not be available from isolate + // directive's linking fn during linking phase attr[name] = interpolateFn(scope); + ($$observers[name] || ($$observers[name] = [])).$$inter = true; (attr.$$observers && attr.$$observers[name].$$scope || scope). $watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) { @@ -6900,7 +8104,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //skip animations when the first digest occurs (when //both the new and the old values are the same) since //the CSS classes are the non-interpolated values - if(name === 'class' && newValue != oldValue) { + if (name === 'class' && newValue != oldValue) { attr.$updateClass(newValue, oldValue); } else { attr.$set(name, newValue); @@ -6930,7 +8134,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { i, ii; if ($rootElement) { - for(i = 0, ii = $rootElement.length; i < ii; i++) { + for (i = 0, ii = $rootElement.length; i < ii; i++) { if ($rootElement[i] == firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, @@ -6943,6 +8147,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } $rootElement.length -= removeCount - 1; + + // If the replaced element is also the jQuery .context then replace it + // .context is a deprecated jQuery api, so we should set it only when jQuery set it + // http://api.jquery.com/context/ + if ($rootElement.context === firstElementToRemove) { + $rootElement.context = newNode; + } break; } } @@ -6951,9 +8162,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parent) { parent.replaceChild(newNode, firstElementToRemove); } + + // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it? var fragment = document.createDocumentFragment(); fragment.appendChild(firstElementToRemove); - newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; + + // 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]); + } + for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { var element = elementsToRemove[k]; jqLite(element).remove(); // must do this way to clean up expando @@ -6969,19 +8204,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function cloneAndAnnotateFn(fn, annotation) { return extend(function() { return fn.apply(null, arguments); }, fn, annotation); } + + + function invokeLinkFn(linkFn, scope, $element, attrs, controllers, transcludeFn) { + try { + linkFn(scope, $element, attrs, controllers, transcludeFn); + } catch (e) { + $exceptionHandler(e, startingTag($element)); + } + } }]; } -var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; +var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; /** * Converts all accepted directives format into proper directive name. - * All of these will become 'myDirective': - * my:Directive - * my-directive - * x-my-directive - * data-my:directive - * - * Also there is special case for Moz prefix starting with upper case letter. * @param name Name to normalize */ function directiveNormalize(name) { @@ -6997,21 +8234,25 @@ function directiveNormalize(name) { * element attributes. The values reflect current binding state `{{ }}`. The normalization is * needed since all of these are treated as equivalent in Angular: * + * ``` * <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a"> + * ``` */ /** * @ngdoc property * @name $compile.directive.Attributes#$attr - * @returns {object} A map of DOM element attribute names to the normalized name. This is - * needed to do reverse lookup from normalized name back to actual name. + * + * @description + * A map of DOM element attribute names to the normalized name. This is + * needed to do reverse lookup from normalized name back to actual name. */ /** * @ngdoc method * @name $compile.directive.Attributes#$set - * @function + * @kind function * * @description * Set DOM element attribute value. @@ -7034,7 +8275,7 @@ function nodesetLinkingFn( /* NodeList */ nodeList, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn -){} +) {} function directiveLinkingFn( /* nodesetLinkingFn */ nodesetLinkingFn, @@ -7042,7 +8283,7 @@ function directiveLinkingFn( /* Node */ node, /* Element */ rootElement, /* function(Function) */ boundTranscludeFn -){} +) {} function tokenDifference(str1, str2) { var values = '', @@ -7050,16 +8291,33 @@ function tokenDifference(str1, str2) { tokens2 = str2.split(/\s+/); outer: - for(var i = 0; i < tokens1.length; i++) { + for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; + for (var j = 0; j < tokens2.length; j++) { + if (token == tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } return values; } +function removeComments(jqNodes) { + jqNodes = jqLite(jqNodes); + var i = jqNodes.length; + + if (i <= 1) { + return jqNodes; + } + + while (i--) { + var node = jqNodes[i]; + if (node.nodeType === NODE_TYPE_COMMENT) { + splice.call(jqNodes, i, 1); + } + } + return jqNodes; +} + /** * @ngdoc provider * @name $controllerProvider @@ -7072,6 +8330,7 @@ function tokenDifference(str1, str2) { */ function $ControllerProvider() { var controllers = {}, + globals = false, CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; @@ -7092,6 +8351,15 @@ function $ControllerProvider() { } }; + /** + * @ngdoc method + * @name $controllerProvider#allowGlobals + * @description If called, allows `$controller` to find controller constructors on `window` + */ + this.allowGlobals = function() { + globals = true; + }; + this.$get = ['$injector', '$window', function($injector, $window) { @@ -7106,7 +8374,12 @@ function $ControllerProvider() { * * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor - * * check `window[constructor]` on the global `window` object + * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global + * `window` object (not recommended) + * + * The string can use the `controller as property` syntax, where the controller instance is published + * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this + * to work correctly. * * @param {Object} locals Injection locals for Controller. * @return {Object} Instance of given controller. @@ -7117,34 +8390,78 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals) { + return function(expression, locals, later, ident) { + // PRIVATE API: + // param `later` --- indicates that the controller's constructor is invoked at a later time. + // If true, $controller will allocate the object with the correct + // prototype chain, but will not invoke the controller until a returned + // callback is invoked. + // param `ident` --- An optional label which overrides the label parsed from the controller + // expression, if any. var instance, match, constructor, identifier; + later = later === true; + if (ident && isString(ident)) { + identifier = ident; + } - if(isString(expression)) { + if (isString(expression)) { match = expression.match(CNTRL_REG), constructor = match[1], - identifier = match[3]; + identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || getter($window, constructor, true); + : getter(locals.$scope, constructor, true) || + (globals ? getter($window, constructor, true) : undefined); assertArgFn(expression, constructor, true); } - instance = $injector.instantiate(expression, locals); - - if (identifier) { - if (!(locals && typeof locals.$scope == 'object')) { - throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", - constructor || expression.name, identifier); + if (later) { + // Instantiate controller later: + // This machinery is used to create an instance of the object before calling the + // controller's constructor itself. + // + // This allows properties to be added to the controller before the constructor is + // invoked. Primarily, this is used for isolate scope bindings in $compile. + // + // This feature is not intended for use by applications, and is thus not documented + // publicly. + // Object creation: http://jsperf.com/create-constructor/2 + var controllerPrototype = (isArray(expression) ? + expression[expression.length - 1] : expression).prototype; + instance = Object.create(controllerPrototype); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } - locals.$scope[identifier] = instance; + return extend(function() { + $injector.invoke(expression, instance, locals, constructor); + return instance; + }, { + instance: instance, + identifier: identifier + }); + } + + instance = $injector.instantiate(expression, locals, constructor); + + if (identifier) { + addIdentifier(locals, identifier, instance, constructor || expression.name); } return instance; }; + + function addIdentifier(locals, identifier, instance, name) { + if (!(locals && isObject(locals.$scope))) { + throw minErr('$controller')('noscp', + "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + name, identifier); + } + + locals.$scope[identifier] = instance; + } }]; } @@ -7157,23 +8474,24 @@ function $ControllerProvider() { * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - <example> + <example module="documentExample"> <file name="index.html"> - <div ng-controller="MainCtrl"> + <div ng-controller="ExampleController"> <p>$document title: <b ng-bind="title"></b></p> <p>window.document title: <b ng-bind="windowTitle"></b></p> </div> </file> <file name="script.js"> - function MainCtrl($scope, $document) { - $scope.title = $document[0].title; - $scope.windowTitle = angular.element(window.document)[0].title; - } + angular.module('documentExample', []) + .controller('ExampleController', ['$scope', '$document', function($scope, $document) { + $scope.title = $document[0].title; + $scope.windowTitle = angular.element(window.document)[0].title; + }]); </file> </example> */ -function $DocumentProvider(){ - this.$get = ['$window', function(window){ +function $DocumentProvider() { + this.$get = ['$window', function(window) { return jqLite(window.document); }]; } @@ -7194,8 +8512,8 @@ function $DocumentProvider(){ * ## Example: * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { - * return function (exception, cause) { + * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { + * return function(exception, cause) { * exception.message += ' (caused by "' + cause + '")'; * throw exception; * }; @@ -7205,6 +8523,14 @@ function $DocumentProvider(){ * This example will override the normal action of `$exceptionHandler`, to make angular * exceptions fail hard when they happen, instead of just logging to the console. * + * <hr /> + * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` + * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} + * (unless executed during a digest). + * + * If you wish, you can manually delegate exceptions, e.g. + * `try { ... } catch(e) { $exceptionHandler(e); }` + * * @param {Error} exception Exception associated with the error. * @param {string=} cause optional information about the context in which * the error was thrown. @@ -7218,6 +8544,36 @@ function $ExceptionHandlerProvider() { }]; } +var APPLICATION_JSON = 'application/json'; +var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'}; +var JSON_START = /^\[|^\{(?!\{)/; +var JSON_ENDS = { + '[': /]$/, + '{': /}$/ +}; +var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; + +function defaultHttpResponseTransform(data, headers) { + if (isString(data)) { + // Strip json vulnerability protection prefix and trim whitespace + var tempData = data.replace(JSON_PROTECTION_PREFIX, '').trim(); + + if (tempData) { + var contentType = headers('Content-Type'); + if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { + data = fromJson(tempData); + } + } + } + + return data; +} + +function isJsonLike(str) { + var jsonStart = str.match(JSON_START); + return jsonStart && JSON_ENDS[jsonStart[0]].test(str); +} + /** * Parse headers into key value object * @@ -7225,7 +8581,7 @@ function $ExceptionHandlerProvider() { * @returns {Object} Parsed headers as key value object */ function parseHeaders(headers) { - var parsed = {}, key, val, i; + var parsed = createMap(), key, val, i; if (!headers) return parsed; @@ -7235,11 +8591,7 @@ function parseHeaders(headers) { val = trim(line.substr(i + 1)); if (key) { - if (parsed[key]) { - parsed[key] += ', ' + val; - } else { - parsed[key] = val; - } + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; } }); @@ -7266,7 +8618,11 @@ function headersGetter(headers) { if (!headersObj) headersObj = parseHeaders(headers); if (name) { - return headersObj[lowercase(name)] || null; + var value = headersObj[lowercase(name)]; + if (value === void 0) { + value = null; + } + return value; } return headersObj; @@ -7280,16 +8636,17 @@ function headersGetter(headers) { * This function is used for both request and response transforming * * @param {*} data Data to transform. - * @param {function(string=)} headers Http headers getter fn. + * @param {function(string=)} headers HTTP headers getter fn. + * @param {number} status HTTP status code of the response. * @param {(Function|Array.<Function>)} fns Function or an array of functions. * @returns {*} Transformed data. */ -function transformData(data, headers, fns) { +function transformData(data, headers, status, fns) { if (isFunction(fns)) - return fns(data, headers); + return fns(data, headers, status); forEach(fns, function(fn) { - data = fn(data, headers); + data = fn(data, headers, status); }); return data; @@ -7301,27 +8658,47 @@ function isSuccess(status) { } +/** + * @ngdoc provider + * @name $httpProvider + * @description + * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. + * */ function $HttpProvider() { - var JSON_START = /^\s*(\[|\{[^\{])/, - JSON_END = /[\}\]]\s*$/, - PROTECTION_PREFIX = /^\)\]\}',?\n/, - CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; - + /** + * @ngdoc property + * @name $httpProvider#defaults + * @description + * + * 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.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. + * Defaults value is `'XSRF-TOKEN'`. + * + * - **`defaults.xsrfHeaderName`** - {string} - Name of HTTP header to populate with the + * XSRF token. Defaults value is `'X-XSRF-TOKEN'`. + * + * - **`defaults.headers`** - {Object} - Default headers for all $http requests. + * Refer to {@link ng.$http#setting-http-headers $http} for documentation on + * setting default headers. + * - **`defaults.headers.common`** + * - **`defaults.headers.post`** + * - **`defaults.headers.put`** + * - **`defaults.headers.patch`** + * + **/ var defaults = this.defaults = { // transform incoming response data - transformResponse: [function(data) { - if (isString(data)) { - // strip json vulnerability protection prefix - data = data.replace(PROTECTION_PREFIX, ''); - if (JSON_START.test(data) && JSON_END.test(data)) - data = fromJson(data); - } - return data; - }], + transformResponse: [defaultHttpResponseTransform], // transform outgoing request data transformRequest: [function(d) { - return isObject(d) && !isFile(d) && !isBlob(d) ? toJson(d) : d; + return isObject(d) && !isFile(d) && !isBlob(d) && !isFormData(d) ? toJson(d) : d; }], // default headers @@ -7329,26 +8706,57 @@ function $HttpProvider() { common: { 'Accept': 'application/json, text/plain, */*' }, - post: copy(CONTENT_TYPE_APPLICATION_JSON), - put: copy(CONTENT_TYPE_APPLICATION_JSON), - patch: copy(CONTENT_TYPE_APPLICATION_JSON) + post: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + put: shallowCopy(CONTENT_TYPE_APPLICATION_JSON), + patch: shallowCopy(CONTENT_TYPE_APPLICATION_JSON) }, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN' }; + var useApplyAsync = false; /** - * Are ordered by request, i.e. they are applied in the same order as the - * array, on request, but reverse order, on response. - */ - var interceptorFactories = this.interceptors = []; + * @ngdoc method + * @name $httpProvider#useApplyAsync + * @description + * + * Configure $http service to combine processing of multiple http responses received at around + * the same time via {@link ng.$rootScope.Scope#$applyAsync $rootScope.$applyAsync}. This can result in + * 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. + * + * @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 + * to load and share the same digest cycle. + * + * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. + * otherwise, returns the current configured value. + **/ + this.useApplyAsync = function(value) { + if (isDefined(value)) { + useApplyAsync = !!value; + return this; + } + return useApplyAsync; + }; /** - * For historical reasons, response interceptors are ordered by the order in which - * they are applied to the response. (This is the opposite of interceptorFactories) - */ - var responseInterceptorFactories = this.responseInterceptors = []; + * @ngdoc property + * @name $httpProvider#interceptors + * @description + * + * Array containing service factories for all synchronous or asynchronous {@link ng.$http $http} + * pre-processing of request or postprocessing of responses. + * + * These service factories are ordered by request, i.e. they are applied in the same order as the + * array, on request, but reverse order, on response. + * + * {@link ng.$http#interceptors Interceptors detailed info} + **/ + var interceptorFactories = this.interceptors = []; this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { @@ -7367,27 +8775,6 @@ function $HttpProvider() { ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); - forEach(responseInterceptorFactories, function(interceptorFactory, index) { - var responseFn = isString(interceptorFactory) - ? $injector.get(interceptorFactory) - : $injector.invoke(interceptorFactory); - - /** - * Response interceptors go before "around" interceptors (no real reason, just - * had to pick one.) But they are already reversed, so we can't use unshift, hence - * the splice. - */ - reversedInterceptors.splice(index, 0, { - response: function(response) { - return responseFn($q.when(response)); - }, - responseError: function(response) { - return responseFn($q.reject(response)); - } - }); - }); - - /** * @ngdoc service * @kind function @@ -7414,13 +8801,27 @@ function $HttpProvider() { * it is important to familiarize yourself with these APIs and the guarantees they provide. * * - * # General usage + * ## 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 - * $http({method: 'GET', url: '/someUrl'}). + * // 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. + * }); + * ``` + * + * ```js + * // Simple POST request example (passing data) : + * $http.post('/someUrl', {msg:'hello word!'}). * success(function(data, status, headers, config) { * // this callback will be called asynchronously * // when the response is available @@ -7431,6 +8832,7 @@ function $HttpProvider() { * }); * ``` * + * * 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 @@ -7441,7 +8843,7 @@ function $HttpProvider() { * XMLHttpRequest will transparently follow it, meaning that the error callback will not be * called for such responses. * - * # Writing Unit Tests that use $http + * ## 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. @@ -7452,7 +8854,7 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * # Shortcut methods + * ## 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. @@ -7470,9 +8872,10 @@ function $HttpProvider() { * - {@link ng.$http#put $http.put} * - {@link ng.$http#delete $http.delete} * - {@link ng.$http#jsonp $http.jsonp} + * - {@link ng.$http#patch $http.patch} * * - * # Setting HTTP Headers + * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults * can be fully configured by accessing the `$httpProvider.defaults.headers` configuration @@ -7502,37 +8905,85 @@ function $HttpProvider() { * In addition, you can supply a `headers` property in the config object passed when * calling `$http(config)`, which overrides the defaults without changing them globally. * + * To explicitly remove a header automatically added via $httpProvider.defaults.headers on a per request basis, + * Use the `headers` property, setting the desired header to `undefined`. For example: + * + * ```js + * var req = { + * method: 'POST', + * url: 'http://example.com', + * headers: { + * 'Content-Type': undefined + * }, + * data: { test: 'test' }, + * } + * + * $http(req).success(function(){...}).error(function(){...}); + * ``` + * + * ## Transforming Requests and Responses + * + * Both requests and responses can be transformed using transformation functions: `transformRequest` + * and `transformResponse`. These properties can be a single function that returns + * 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. * - * # Transforming Requests and Responses + * ### Default Transformations * - * Both requests and responses can be transformed using transform functions. By default, Angular - * applies these transformations: + * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and + * `defaults.transformResponse` properties. If a request does not provide its own transformations + * then these will be applied. * - * Request transformations: + * You can augment or replace the default transformations by modifying these properties by adding to or + * replacing the array. + * + * Angular provides the following default transformations: + * + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations: + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): * * - If XSRF prefix is detected, strip it (see Security Considerations section below). * - If JSON response is detected, deserialize it using a JSON parser. * - * To globally augment or override the default transforms, modify the - * `$httpProvider.defaults.transformRequest` and `$httpProvider.defaults.transformResponse` - * properties. These properties are by default an array of transform functions, which allows you - * to `push` or `unshift` a new transformation function into the transformation chain. You can - * also decide to completely override any default transformations by assigning your - * transformation functions to these properties directly without the array wrapper. These defaults - * are again available on the $http factory at run-time, which may be useful if you have run-time - * services you wish to be involved in your transformations. * - * Similarly, to locally override the request/response transforms, augment the - * `transformRequest` and/or `transformResponse` properties of the configuration object passed + * ### Overriding the Default Transformations Per Request + * + * If you wish override the request/response transformations only for a single request then provide + * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * + * Note that if you provide these properties on the config object the default transformations will be + * overwritten. If you wish to augment the default transformations then you must include them in your + * local transformation array. + * + * The following code demonstrates adding a new response transformation to be run after the default response + * transformations have been run. + * + * ```js + * function appendTransform(defaults, transform) { + * + * // We can't guarantee that the default transformation is an array + * defaults = angular.isArray(defaults) ? defaults : [defaults]; * - * # Caching + * // Append the new transformation to the defaults + * return defaults.concat(transform); + * } + * + * $http({ + * url: '...', + * method: 'GET', + * transformResponse: appendTransform($http.defaults.transformResponse, function(value) { + * return doTransform(value); + * }) + * }); + * ``` + * + * + * ## 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`}). @@ -7549,13 +9000,13 @@ function $HttpProvider() { * * You can change the default cache to a new object (built with * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#properties_defaults `$http.defaults.cache`} property. All requests who set + * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set * their `cache` property to `true` will now use this cache object. * * If you set the default cache to `false` then only requests that specify their own custom * cache object will be cached. * - * # Interceptors + * ## Interceptors * * Before you start creating interceptors, be sure to understand the * {@link ng.$q $q and deferred/promise APIs}. @@ -7573,14 +9024,14 @@ function $HttpProvider() { * * There are two kinds of interceptors (and two kinds of rejection interceptors): * - * * `request`: interceptors get called with http `config` object. The function is free to - * modify the `config` or create a new one. The function needs to return the `config` - * directly or as a promise. + * * `request`: interceptors get called with a http `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 * resolved with a rejection. * * `response`: interceptors get called with http `response` object. The function is free to - * modify the `response` or create a new one. The function needs to return the `response` - * directly or as a promise. + * modify the `response` object or create a new one. The function needs to return the `response` + * object directly, or as a promise containing the `response` or a new `response` object. * * `responseError`: interceptor gets called when a previous interceptor threw an error or * resolved with a rejection. * @@ -7592,7 +9043,7 @@ function $HttpProvider() { * // optional method * 'request': function(config) { * // do something on success - * return config || $q.when(config); + * return config; * }, * * // optional method @@ -7609,7 +9060,7 @@ function $HttpProvider() { * // optional method * 'response': function(response) { * // do something on success - * return response || $q.when(response); + * return response; * }, * * // optional method @@ -7640,52 +9091,7 @@ function $HttpProvider() { * }); * ``` * - * # Response interceptors (DEPRECATED) - * - * Before you start creating interceptors, be sure to understand the - * {@link ng.$q $q and deferred/promise APIs}. - * - * For purposes of global error handling, authentication or any kind of synchronous or - * asynchronous preprocessing of received responses, it is desirable to be able to intercept - * responses for http requests before they are handed over to the application code that - * initiated these requests. The response interceptors leverage the {@link ng.$q - * promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. - * - * The interceptors are service factories that are registered with the $httpProvider by - * adding them to the `$httpProvider.responseInterceptors` array. The factory is called and - * injected with dependencies (if specified) and returns the interceptor — a function that - * takes a {@link ng.$q promise} and returns the original or a new promise. - * - * ```js - * // register the interceptor as a service - * $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { - * return function(promise) { - * return promise.then(function(response) { - * // do something on success - * return response; - * }, function(response) { - * // do something on error - * if (canRecover(response)) { - * return responseOrNewPromise - * } - * return $q.reject(response); - * }); - * } - * }); - * - * $httpProvider.responseInterceptors.push('myHttpInterceptor'); - * - * - * // register the interceptor via an anonymous factory - * $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { - * return function(promise) { - * // same as above - * } - * }); - * ``` - * - * - * # Security Considerations + * ## Security Considerations * * When designing web applications, consider security threats from: * @@ -7696,7 +9102,7 @@ function $HttpProvider() { * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * - * ## JSON Vulnerability Protection + * ### JSON Vulnerability Protection * * A [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * allows third party website to turn your JSON resource URL into @@ -7718,7 +9124,7 @@ function $HttpProvider() { * Angular will strip the prefix, before processing the JSON. * * - * ## Cross Site Request Forgery (XSRF) Protection + * ### 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 @@ -7734,7 +9140,7 @@ function $HttpProvider() { * that only JavaScript running on your domain could have sent the request. The token must be * unique for each user and must be verifiable by the server (to prevent the JavaScript from * making up its own tokens). We recommend that the token is a digest of your site's - * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) + * authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) * for added security. * * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName @@ -7760,18 +9166,22 @@ function $HttpProvider() { * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – * transform function or an array of such functions. The transform function takes the http * request body and headers and returns its transformed (typically serialized) version. + * See {@link ng.$http#overriding-the-default-transformations-per-request + * Overriding the Default Transformations} * - **transformResponse** – - * `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – + * `{function(data, headersGetter, status)|Array.<function(data, headersGetter, status)>}` – * transform function or an array of such functions. The transform function takes the http - * response body and headers and returns its transformed (typically deserialized) version. + * 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. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. - * - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the - * XHR object. See [requests with credentials]https://developer.mozilla.org/en/http_access_control#section_5 + * - **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). @@ -7796,9 +9206,9 @@ function $HttpProvider() { * * * @example -<example> +<example module="httpExample"> <file name="index.html"> - <div ng-controller="FetchCtrl"> + <div ng-controller="FetchController"> <select ng-model="method"> <option>GET</option> <option>JSONP</option> @@ -7808,11 +9218,11 @@ function $HttpProvider() { <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> <button id="samplejsonpbtn" ng-click="updateModel('JSONP', - 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> + 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> Sample JSONP </button> <button id="invalidjsonpbtn" - ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> + ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> Invalid JSONP </button> <pre>http status code: {{status}}</pre> @@ -7820,30 +9230,32 @@ function $HttpProvider() { </div> </file> <file name="script.js"> - function FetchCtrl($scope, $http, $templateCache) { - $scope.method = 'GET'; - $scope.url = 'http-hello.html'; - - $scope.fetch = function() { - $scope.code = null; - $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; - }); - }; + angular.module('httpExample', []) + .controller('FetchController', ['$scope', '$http', '$templateCache', + function($scope, $http, $templateCache) { + $scope.method = 'GET'; + $scope.url = 'http-hello.html'; + + $scope.fetch = function() { + $scope.code = null; + $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; + }); + }; - $scope.updateModel = function(method, url) { - $scope.method = method; - $scope.url = url; - }; - } + $scope.updateModel = function(method, url) { + $scope.method = method; + $scope.url = url; + }; + }]); </file> <file name="http-hello.html"> Hello, $http! @@ -7863,12 +9275,13 @@ function $HttpProvider() { expect(data.getText()).toMatch(/Hello, \$http!/); }); - it('should make a JSONP request to angularjs.org', function() { - sampleJsonpBtn.click(); - fetchBtn.click(); - expect(status.getText()).toMatch('200'); - expect(data.getText()).toMatch(/Super Hero!/); - }); +// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 +// it('should make a JSONP request to angularjs.org', function() { +// sampleJsonpBtn.click(); +// fetchBtn.click(); +// expect(status.getText()).toMatch('200'); +// expect(data.getText()).toMatch(/Super Hero!/); +// }); it('should make JSONP request to invalid URL and invoke the error handler', function() { @@ -7881,31 +9294,26 @@ function $HttpProvider() { </example> */ function $http(requestConfig) { - var config = { + + if (!angular.isObject(requestConfig)) { + throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); + } + + var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse - }; - var headers = mergeHeaders(requestConfig); + }, requestConfig); - extend(config, requestConfig); - config.headers = headers; + config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); - var xsrfValue = urlIsSameOrigin(config.url) - ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] - : undefined; - if (xsrfValue) { - headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; - } - - var serverRequest = function(config) { - headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); + var headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); // strip content-type if data is undefined - if (isUndefined(config.data)) { + if (isUndefined(reqData)) { forEach(headers, function(value, header) { if (lowercase(header) === 'content-type') { delete headers[header]; @@ -7918,7 +9326,7 @@ function $HttpProvider() { } // send request - return sendReq(config, reqData, headers).then(transformResponse, transformResponse); + return sendReq(config, reqData).then(transformResponse, transformResponse); }; var chain = [serverRequest, undefined]; @@ -7934,7 +9342,7 @@ function $HttpProvider() { } }); - while(chain.length) { + while (chain.length) { var thenFn = chain.shift(); var rejectFn = chain.shift(); @@ -7959,14 +9367,34 @@ function $HttpProvider() { function transformResponse(response) { // make a copy since the response must be cacheable - var resp = extend({}, response, { - data: transformData(response.data, response.headers, config.transformResponse) - }); + var resp = extend({}, response); + if (!response.data) { + resp.data = response.data; + } else { + resp.data = transformData(response.data, response.headers, response.status, config.transformResponse); + } return (isSuccess(response.status)) ? resp : $q.reject(resp); } + function executeHeaderFns(headers) { + var headerContent, processedHeaders = {}; + + forEach(headers, function(headerFn, header) { + if (isFunction(headerFn)) { + headerContent = headerFn(); + if (headerContent != null) { + processedHeaders[header] = headerContent; + } + } else { + processedHeaders[header] = headerFn; + } + }); + + return processedHeaders; + } + function mergeHeaders(config) { var defHeaders = defaults.headers, reqHeaders = extend({}, config.headers), @@ -7974,10 +9402,6 @@ function $HttpProvider() { defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // execute if header value is function - execHeaders(defHeaders); - execHeaders(reqHeaders); - // using for-in instead of forEach to avoid unecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { @@ -7992,22 +9416,8 @@ function $HttpProvider() { reqHeaders[defHeaderName] = defHeaders[defHeaderName]; } - return reqHeaders; - - function execHeaders(headers) { - var headerContent; - - forEach(headers, function(headerFn, header) { - if (isFunction(headerFn)) { - headerContent = headerFn(); - if (headerContent != null) { - headers[header] = headerContent; - } else { - delete headers[header]; - } - } - }); - } + // execute if header value is a function for merged headers + return executeHeaderFns(reqHeaders); } } @@ -8057,7 +9467,7 @@ function $HttpProvider() { * Shortcut method to perform `JSONP` request. * * @param {string} url Relative or absolute URL specifying the destination of the request. - * Should contain `JSON_CALLBACK` string. + * The name of the callback should be the string `JSON_CALLBACK`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -8088,7 +9498,20 @@ function $HttpProvider() { * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ - createShortMethodsWithData('post', 'put'); + + /** + * @ngdoc method + * @name $http#patch + * + * @description + * Shortcut method to perform `PATCH` request. + * + * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {*} data Request content + * @param {Object=} config Optional configuration object + * @returns {HttpPromise} Future object + */ + createShortMethodsWithData('post', 'put', 'patch'); /** * @ngdoc property @@ -8137,18 +9560,20 @@ function $HttpProvider() { * !!! ACCESSES CLOSURE VARS: * $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests */ - function sendReq(config, reqData, reqHeaders) { + function sendReq(config, reqData) { var deferred = $q.defer(), promise = deferred.promise, cache, cachedResp, + reqHeaders = config.headers, url = buildUrl(config.url, config.params); $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { + if ((config.cache || defaults.cache) && config.cache !== false && + (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache : isObject(defaults.cache) ? defaults.cache : defaultCache; @@ -8157,14 +9582,13 @@ function $HttpProvider() { if (cache) { cachedResp = cache.get(url); if (isDefined(cachedResp)) { - if (cachedResp.then) { + if (isPromiseLike(cachedResp)) { // cached request has already been sent, but there is no response yet - cachedResp.then(removePendingReq, removePendingReq); - return cachedResp; + cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult); } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]), cachedResp[3]); + resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]); } else { resolvePromise(cachedResp, 200, {}, 'OK'); } @@ -8175,8 +9599,17 @@ function $HttpProvider() { } } - // if we won't have the response in cache, send the request to the backend + + // if we won't have the response in cache, set the xsrf headers and + // send the request to the backend if (isUndefined(cachedResp)) { + var xsrfValue = urlIsSameOrigin(config.url) + ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] + : undefined; + if (xsrfValue) { + reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; + } + $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, config.withCredentials, config.responseType); } @@ -8200,8 +9633,16 @@ function $HttpProvider() { } } - resolvePromise(response, status, headersString, statusText); - if (!$rootScope.$$phase) $rootScope.$apply(); + function resolveHttpPromise() { + resolvePromise(response, status, headersString, statusText); + } + + if (useApplyAsync) { + $rootScope.$applyAsync(resolveHttpPromise); + } else { + resolveHttpPromise(); + if (!$rootScope.$$phase) $rootScope.$apply(); + } } @@ -8217,55 +9658,50 @@ function $HttpProvider() { status: status, headers: headersGetter(headers), config: config, - statusText : statusText + statusText: statusText }); } + function resolvePromiseWithResult(result) { + resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText); + } function removePendingReq() { - var idx = indexOf($http.pendingRequests, config); + var idx = $http.pendingRequests.indexOf(config); if (idx !== -1) $http.pendingRequests.splice(idx, 1); } } 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)) { - v = toJson(v); - } - parts.push(encodeUriQuery(key) + '=' + - encodeUriQuery(v)); - }); - }); - if(parts.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + 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); + } } - return url; - } - - + parts.push(encodeUriQuery(key) + '=' + + encodeUriQuery(v)); + }); + }); + if (parts.length > 0) { + url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); + } + return url; + } }]; } -function createXhr(method) { - //if IE and the method is not RFC2616 compliant, or if XMLHttpRequest - //is not available, try getting an ActiveXObject. Otherwise, use XMLHttpRequest - //if it is available - if (msie <= 8 && (!method.match(/^(get|post|head|put|delete|options)$/i) || - !window.XMLHttpRequest)) { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } else if (window.XMLHttpRequest) { - return new window.XMLHttpRequest(); - } - - throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); +function createXhr() { + return new window.XMLHttpRequest(); } /** @@ -8291,11 +9727,8 @@ function $HttpBackendProvider() { } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { - var ABORTED = -1; - // TODO(vojta): fix the signature return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - var status; $browser.$$incOutstandingRequestCount(); url = url || $browser.url(); @@ -8303,20 +9736,17 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc var callbackId = '_' + (callbacks.counter++).toString(36); callbacks[callbackId] = function(data) { callbacks[callbackId].data = data; + callbacks[callbackId].called = true; }; var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - function() { - if (callbacks[callbackId].data) { - completeRequest(callback, 200, callbacks[callbackId].data); - } else { - completeRequest(callback, status || -2); - } - callbacks[callbackId] = angular.noop; + callbackId, function(status, text) { + completeRequest(callback, status, callbacks[callbackId].data, "", text); + callbacks[callbackId] = noop; }); } else { - var xhr = createXhr(method); + var xhr = createXhr(); xhr.open(method, url, true); forEach(headers, function(value, key) { @@ -8325,37 +9755,39 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }); - // In IE6 and 7, this might be called synchronously when xhr.send below is called and the - // response is in the cache. the promise api will ensure that to the app code the api is - // always async - xhr.onreadystatechange = function() { - // onreadystatechange might get called multiple times with readyState === 4 on mobile webkit caused by - // xhrs that are resolved while the app is in the background (see #5426). - // since calling completeRequest sets the `xhr` variable to null, we just check if it's not null before - // continuing - // - // we can't set xhr.onreadystatechange to undefined or delete it because that breaks IE8 (method=PATCH) and - // Safari respectively. - if (xhr && xhr.readyState == 4) { - var responseHeaders = null, - response = null; - - if(status !== ABORTED) { - responseHeaders = xhr.getAllResponseHeaders(); - - // responseText is the old-school way of retrieving response (supported by IE8 & 9) - // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) - response = ('response' in xhr) ? xhr.response : xhr.responseText; - } + xhr.onload = function requestLoaded() { + var statusText = xhr.statusText || ''; + + // responseText is the old-school way of retrieving response (supported by IE8 & 9) + // response/responseType properties were introduced in XHR Level2 spec (supported by IE10) + var response = ('response' in xhr) ? xhr.response : xhr.responseText; + + // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status === 1223 ? 204 : xhr.status; - completeRequest(callback, - status || xhr.status, - response, - responseHeaders, - xhr.statusText || ''); + // fix status code when it is 0 (0 status is undocumented). + // Occurs when accessing file resources or on Android 4.1 stock browser + // while retrieving files from application cache. + if (status === 0) { + status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; } + + completeRequest(callback, + status, + response, + xhr.getAllResponseHeaders(), + statusText); + }; + + var requestError = function() { + // The response is always empty + // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error + completeRequest(callback, -1, null, null, ''); }; + xhr.onerror = requestError; + xhr.onabort = requestError; + if (withCredentials) { xhr.withCredentials = true; } @@ -8382,66 +9814,62 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc if (timeout > 0) { var timeoutId = $browserDefer(timeoutRequest, timeout); - } else if (timeout && timeout.then) { + } else if (isPromiseLike(timeout)) { timeout.then(timeoutRequest); } function timeoutRequest() { - status = ABORTED; jsonpDone && jsonpDone(); xhr && xhr.abort(); } function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution - timeoutId && $browserDefer.cancel(timeoutId); - jsonpDone = xhr = null; - - // fix status code when it is 0 (0 status is undocumented). - // Occurs when accessing file resources or on Android 4.1 stock browser - // while retrieving files from application cache. - if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + if (timeoutId !== undefined) { + $browserDefer.cancel(timeoutId); } - - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status === 1223 ? 204 : status; - statusText = statusText || ''; + jsonpDone = xhr = null; callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; - function jsonpReq(url, done) { + function jsonpReq(url, callbackId, done) { // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'), - doneWrapper = function() { - script.onreadystatechange = script.onload = script.onerror = null; - rawDocument.body.removeChild(script); - if (done) done(); - }; - - script.type = 'text/javascript'; + var script = rawDocument.createElement('script'), callback = null; + script.type = "text/javascript"; script.src = url; - - if (msie && msie <= 8) { - script.onreadystatechange = function() { - if (/loaded|complete/.test(script.readyState)) { - doneWrapper(); + script.async = true; + + callback = function(event) { + removeEventListenerFn(script, "load", callback); + removeEventListenerFn(script, "error", callback); + rawDocument.body.removeChild(script); + script = null; + var status = -1; + var text = "unknown"; + + if (event) { + if (event.type === "load" && !callbacks[callbackId].called) { + event = { type: "error" }; } - }; - } else { - script.onload = script.onerror = function() { - doneWrapper(); - }; - } + text = event.type; + status = event.type === "error" ? 404 : 200; + } + if (done) { + done(status, text); + } + }; + + addEventListenerFn(script, "load", callback); + addEventListenerFn(script, "error", callback); rawDocument.body.appendChild(script); - return doneWrapper; + return callback; } } @@ -8450,7 +9878,6 @@ var $interpolateMinErr = minErr('$interpolate'); /** * @ngdoc provider * @name $interpolateProvider - * @function * * @description * @@ -8468,7 +9895,7 @@ var $interpolateMinErr = minErr('$interpolate'); }); - customInterpolationApp.controller('DemoController', function DemoController() { + customInterpolationApp.controller('DemoController', function() { this.label = "This binding is brought you by // interpolation symbols."; }); </script> @@ -8496,7 +9923,7 @@ function $InterpolateProvider() { * @param {string=} value new value to set the starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value) { if (value) { startSymbol = value; return this; @@ -8514,7 +9941,7 @@ function $InterpolateProvider() { * @param {string=} value new value to set the ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value) { if (value) { endSymbol = value; return this; @@ -8526,12 +9953,18 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } /** * @ngdoc service * @name $interpolate - * @function + * @kind function * * @requires $parse * @requires $sce @@ -8550,6 +9983,62 @@ function $InterpolateProvider() { * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!'); * ``` * + * `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is + * `true`, the interpolation function will return `undefined` unless all embedded expressions + * evaluate to a value other than `undefined`. + * + * ```js + * var $interpolate = ...; // injected + * var context = {greeting: 'Hello', name: undefined }; + * + * // default "forgiving" mode + * var exp = $interpolate('{{greeting}} {{name}}!'); + * expect(exp(context)).toEqual('Hello !'); + * + * // "allOrNothing" mode + * exp = $interpolate('{{greeting}} {{name}}!', false, null, true); + * expect(exp(context)).toBeUndefined(); + * context.name = 'Angular'; + * expect(exp(context)).toEqual('Hello Angular!'); + * ``` + * + * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. + * + * ####Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * <example> + * <file name="index.html"> + * <div ng-init="username='A user'"> + * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} + * </p> + * <p><strong>{{username}}</strong> attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.</p> + * <p>Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.</p> + * </div> + * </file> + * </example> * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have @@ -8559,89 +10048,139 @@ function $InterpolateProvider() { * result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, * trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that * provides Strict Contextual Escaping for details. + * @param {boolean=} allOrNothing if `true`, then the returned function returns undefined + * unless all embedded expressions evaluate to a value other than `undefined`. * @returns {function(context)} an interpolation function which is used to compute the * interpolated string. The function has these parameters: * - * * `context`: an object against which any expressions embedded in the strings are evaluated - * against. - * + * - `context`: evaluation context for all expressions embedded in the interpolated text */ - function $interpolate(text, mustHaveExpression, trustedContext) { + function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) { + allOrNothing = !!allOrNothing; var startIndex, endIndex, index = 0, - parts = [], - length = text.length, - hasInterpolation = false, - fn, + expressions = [], + parseFns = [], + textLength = text.length, exp, - concat = []; - - while(index < length) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { - (index != startIndex) && parts.push(text.substring(index, startIndex)); - parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); - fn.exp = exp; + concat = [], + expressionPositions = []; + + while (index < textLength) { + if (((startIndex = text.indexOf(startSymbol, index)) != -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { + if (index !== startIndex) { + concat.push(unescapeText(text.substring(index, startIndex))); + } + exp = text.substring(startIndex + startSymbolLength, endIndex); + expressions.push(exp); + parseFns.push($parse(exp, parseStringifyInterceptor)); index = endIndex + endSymbolLength; - hasInterpolation = true; + expressionPositions.push(concat.length); + concat.push(''); } else { - // we did not find anything, so we have to add the remainder to the parts array - (index != length) && parts.push(text.substring(index)); - index = length; + // we did not find an interpolation, so we have to add the remainder to the separators array + if (index !== textLength) { + concat.push(unescapeText(text.substring(index))); + } + break; } } - if (!(length = parts.length)) { - // we added, nothing, must have been an empty string. - parts.push(''); - length = 1; - } - // Concatenating expressions makes it hard to reason about whether some combination of // concatenated values are unsafe to use and could easily lead to XSS. By requiring that a // single expression be used for iframe[src], object[src], etc., we ensure that the value // that's used is assigned or constructed by some JS code somewhere that is more testable or // 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 && parts.length > 1) { + 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); } - if (!mustHaveExpression || hasInterpolation) { - concat.length = length; - fn = function(context) { - try { - for(var i = 0, ii = length, part; i<ii; i++) { - if (typeof (part = parts[i]) == 'function') { - part = part(context); - if (trustedContext) { - part = $sce.getTrusted(trustedContext, part); - } else { - part = $sce.valueOf(part); - } - if (part === null || isUndefined(part)) { - part = ''; - } else if (typeof part != 'string') { - part = toJson(part); - } - } - concat[i] = part; - } - return concat.join(''); + if (!mustHaveExpression || expressions.length) { + var compute = function(values) { + for (var i = 0, ii = expressions.length; i < ii; i++) { + if (allOrNothing && isUndefined(values[i])) return; + concat[expressionPositions[i]] = values[i]; + } + return concat.join(''); + }; + + var getValue = function(value) { + return trustedContext ? + $sce.getTrusted(trustedContext, value) : + $sce.valueOf(value); + }; + + var stringify = function(value) { + if (value == null) { // null || undefined + return ''; } - catch(err) { - var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, - err.toString()); - $exceptionHandler(newErr); + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + value = toJson(value); } + + return value; }; - fn.exp = text; - fn.parts = parts; - return fn; + + return extend(function interpolationFn(context) { + var i = 0; + var ii = expressions.length; + var values = new Array(ii); + + try { + for (; i < ii; i++) { + values[i] = parseFns[i](context); + } + + return compute(values); + } catch (err) { + var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, + err.toString()); + $exceptionHandler(newErr); + } + + }, { + // 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) { + var lastValue; + return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + var currValue = compute(values); + if (isFunction(listener)) { + 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); + } } } @@ -8652,7 +10191,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. * - * Use {@link ng.$interpolateProvider#startSymbol $interpolateProvider#startSymbol} to change + * Use {@link ng.$interpolateProvider#startSymbol `$interpolateProvider.startSymbol`} to change * the symbol. * * @returns {string} start symbol. @@ -8668,7 +10207,7 @@ function $InterpolateProvider() { * @description * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * - * Use {@link ng.$interpolateProvider#endSymbol $interpolateProvider#endSymbol} to change + * Use {@link ng.$interpolateProvider#endSymbol `$interpolateProvider.endSymbol`} to change * the symbol. * * @returns {string} end symbol. @@ -8682,8 +10221,8 @@ function $InterpolateProvider() { } function $IntervalProvider() { - this.$get = ['$rootScope', '$window', '$q', - function($rootScope, $window, $q) { + this.$get = ['$rootScope', '$window', '$q', '$$q', + function($rootScope, $window, $q, $$q) { var intervals = {}; @@ -8722,55 +10261,56 @@ function $IntervalProvider() { * @returns {promise} A promise which will be notified on each iteration. * * @example - * <example module="time"> - * <file name="index.html"> - * <script> - * function Ctrl2($scope,$interval) { - * $scope.format = 'M/d/yy h:mm:ss a'; - * $scope.blood_1 = 100; - * $scope.blood_2 = 120; + * <example module="intervalExample"> + * <file name="index.html"> + * <script> + * angular.module('intervalExample', []) + * .controller('ExampleController', ['$scope', '$interval', + * function($scope, $interval) { + * $scope.format = 'M/d/yy h:mm:ss a'; + * $scope.blood_1 = 100; + * $scope.blood_2 = 120; * - * var stop; - * $scope.fight = function() { - * // Don't start a new fight if we are already fighting - * if ( angular.isDefined(stop) ) return; + * var stop; + * $scope.fight = function() { + * // Don't start a new fight if we are already fighting + * if ( angular.isDefined(stop) ) return; * - * stop = $interval(function() { - * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { + * stop = $interval(function() { + * if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { * $scope.blood_1 = $scope.blood_1 - 3; * $scope.blood_2 = $scope.blood_2 - 4; - * } else { + * } else { * $scope.stopFight(); - * } - * }, 100); - * }; + * } + * }, 100); + * }; * - * $scope.stopFight = function() { - * if (angular.isDefined(stop)) { - * $interval.cancel(stop); - * stop = undefined; - * } - * }; - * - * $scope.resetFight = function() { - * $scope.blood_1 = 100; - * $scope.blood_2 = 120; - * } + * $scope.stopFight = function() { + * if (angular.isDefined(stop)) { + * $interval.cancel(stop); + * stop = undefined; + * } + * }; * - * $scope.$on('$destroy', function() { - * // Make sure that the interval is destroyed too - * $scope.stopFight(); - * }); - * } + * $scope.resetFight = function() { + * $scope.blood_1 = 100; + * $scope.blood_2 = 120; + * }; * - * angular.module('time', []) - * // Register the 'myCurrentTime' directive factory method. - * // We inject $interval and dateFilter service since the factory method is DI. - * .directive('myCurrentTime', function($interval, dateFilter) { + * $scope.$on('$destroy', function() { + * // Make sure that the interval is destroyed too + * $scope.stopFight(); + * }); + * }]) + * // Register the 'myCurrentTime' directive factory method. + * // We inject $interval and dateFilter service since the factory method is DI. + * .directive('myCurrentTime', ['$interval', 'dateFilter', + * function($interval, dateFilter) { * // return the directive link function. (compile function not needed) * return function(scope, element, attrs) { * var format, // date format - * stopTime; // so that we can cancel the time updates + * stopTime; // so that we can cancel the time updates * * // used to update the UI * function updateTime() { @@ -8786,37 +10326,37 @@ function $IntervalProvider() { * stopTime = $interval(updateTime, 1000); * * // listen on DOM destroy (removal) event, and cancel the next UI update - * // to prevent updating time ofter the DOM element was removed. - * element.bind('$destroy', function() { + * // to prevent updating time after the DOM element was removed. + * element.on('$destroy', function() { * $interval.cancel(stopTime); * }); * } - * }); - * </script> + * }]); + * </script> * - * <div> - * <div ng-controller="Ctrl2"> - * Date format: <input ng-model="format"> <hr/> - * Current time is: <span my-current-time="format"></span> - * <hr/> - * Blood 1 : <font color='red'>{{blood_1}}</font> - * Blood 2 : <font color='red'>{{blood_2}}</font> - * <button type="button" data-ng-click="fight()">Fight</button> - * <button type="button" data-ng-click="stopFight()">StopFight</button> - * <button type="button" data-ng-click="resetFight()">resetFight</button> - * </div> + * <div> + * <div ng-controller="ExampleController"> + * Date format: <input ng-model="format"> <hr/> + * Current time is: <span my-current-time="format"></span> + * <hr/> + * Blood 1 : <font color='red'>{{blood_1}}</font> + * Blood 2 : <font color='red'>{{blood_2}}</font> + * <button type="button" data-ng-click="fight()">Fight</button> + * <button type="button" data-ng-click="stopFight()">StopFight</button> + * <button type="button" data-ng-click="resetFight()">resetFight</button> * </div> + * </div> * - * </file> + * </file> * </example> */ function interval(fn, delay, count, invokeApply) { var setInterval = $window.setInterval, clearInterval = $window.clearInterval, - deferred = $q.defer(), - promise = deferred.promise, iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply); + skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; count = isDefined(count) ? count : 0; @@ -8854,7 +10394,7 @@ function $IntervalProvider() { interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { intervals[promise.$$intervalId].reject('canceled'); - clearInterval(promise.$$intervalId); + $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; return true; } @@ -8875,7 +10415,7 @@ function $IntervalProvider() { * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -function $LocaleProvider(){ +function $LocaleProvider() { this.$get = function() { return { id: 'en-us', @@ -8918,7 +10458,7 @@ function $LocaleProvider(){ 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', + 'short': 'M/d/yy h:mm a', fullDate: 'EEEE, MMMM d, y', longDate: 'MMMM d, y', mediumDate: 'MMM d, y', @@ -8959,8 +10499,8 @@ function encodePath(path) { return segments.join('/'); } -function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { - var parsedUrl = urlResolve(absoluteUrl, appBase); +function parseAbsoluteUrl(absoluteUrl, locationObj) { + var parsedUrl = urlResolve(absoluteUrl); locationObj.$$protocol = parsedUrl.protocol; locationObj.$$host = parsedUrl.hostname; @@ -8968,12 +10508,12 @@ function parseAbsoluteUrl(absoluteUrl, locationObj, appBase) { } -function parseAppUrl(relativeUrl, locationObj, appBase) { +function parseAppUrl(relativeUrl, locationObj) { var prefixed = (relativeUrl.charAt(0) !== '/'); if (prefixed) { relativeUrl = '/' + relativeUrl; } - var match = urlResolve(relativeUrl, appBase); + var match = urlResolve(relativeUrl); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); locationObj.$$search = parseKeyValue(match.search); @@ -9005,6 +10545,10 @@ function stripHash(url) { return index == -1 ? url : url.substr(0, index); } +function trimEmptyHash(url) { + return url.replace(/(#.+)|#$/, '$1'); +} + function stripFile(url) { return url.substr(0, stripHash(url).lastIndexOf('/') + 1); @@ -9028,12 +10572,12 @@ function LocationHtml5Url(appBase, basePrefix) { this.$$html5 = true; basePrefix = basePrefix || ''; var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** * Parse given html5 (regular) url string into properties - * @param {string} newAbsoluteUrl HTML5 url + * @param {string} url HTML5 url * @private */ this.$$parse = function(url) { @@ -9043,7 +10587,7 @@ function LocationHtml5Url(appBase, basePrefix) { appBaseNoFile); } - parseAppUrl(pathUrl, this, appBase); + parseAppUrl(pathUrl, this); if (!this.$$path) { this.$$path = '/'; @@ -9064,21 +10608,32 @@ function LocationHtml5Url(appBase, basePrefix) { this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' }; - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } var appUrl, prevAppUrl; + var rewrittenUrl; - if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { + if ((appUrl = beginsWith(appBase, url)) !== undefined) { prevAppUrl = appUrl; - if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { - return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) { + rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); } else { - return appBase + prevAppUrl; + rewrittenUrl = appBase + prevAppUrl; } - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { - return appBaseNoFile + appUrl; + } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) { + rewrittenUrl = appBaseNoFile + appUrl; } else if (appBaseNoFile == url + '/') { - return appBaseNoFile; + rewrittenUrl = appBaseNoFile; } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); + } + return !!rewrittenUrl; }; } @@ -9095,7 +10650,7 @@ function LocationHtml5Url(appBase, basePrefix) { function LocationHashbangUrl(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - parseAbsoluteUrl(appBase, this, appBase); + parseAbsoluteUrl(appBase, this); /** @@ -9105,17 +10660,26 @@ function LocationHashbangUrl(appBase, hashPrefix) { */ this.$$parse = function(url) { var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); - var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' - ? beginsWith(hashPrefix, withoutBaseUrl) - : (this.$$html5) - ? withoutBaseUrl - : ''; + var withoutHashUrl; + + if (withoutBaseUrl.charAt(0) === '#') { + + // The rest of the url starts with a hash so we have + // got either a hashbang path or a plain hash fragment + withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); + if (isUndefined(withoutHashUrl)) { + // There was no hashbang prefix so we just have a hash fragment + withoutHashUrl = withoutBaseUrl; + } - if (!isString(withoutHashUrl)) { - throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, - hashPrefix); + } else { + // There was no hashbang path nor hash fragment: + // If we are in HTML5 mode we use what is left as the path; + // Otherwise we ignore what is left + withoutHashUrl = this.$$html5 ? withoutBaseUrl : ''; } - parseAppUrl(withoutHashUrl, this, appBase); + + parseAppUrl(withoutHashUrl, this); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); @@ -9132,12 +10696,12 @@ function LocationHashbangUrl(appBase, hashPrefix) { * Inside of Angular, we're always using pathnames that * do not include drive names for routing. */ - function removeWindowsDriveName (path, url, base) { + function removeWindowsDriveName(path, url, base) { /* Matches paths for file protocol on windows, such as /C:/foo/bar, and captures only /foo/bar. */ - var windowsFilePathExp = /^\/?.*?:(\/.*)/; + var windowsFilePathExp = /^\/[A-Z]:(\/.*)/; var firstPathSegmentMatch; @@ -9146,10 +10710,7 @@ function LocationHashbangUrl(appBase, hashPrefix) { url = url.replace(base, ''); } - /* - * The input URL intentionally contains a - * first path segment that ends with a colon. - */ + // The input URL intentionally contains a first path segment that ends with a colon. if (windowsFilePathExp.exec(url)) { return path; } @@ -9171,10 +10732,12 @@ function LocationHashbangUrl(appBase, hashPrefix) { this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); }; - this.$$rewrite = function(url) { - if(stripHash(appBase) == stripHash(url)) { - return url; + this.$$parseLinkUrl = function(url, relHref) { + if (stripHash(appBase) == stripHash(url)) { + this.$$parse(url); + return true; } + return false; }; } @@ -9194,23 +10757,43 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) { var appBaseNoFile = stripFile(appBase); - this.$$rewrite = function(url) { + this.$$parseLinkUrl = function(url, relHref) { + if (relHref && relHref[0] === '#') { + // special case for links to hash fragments: + // keep the old url and only replace the hash fragment + this.hash(relHref.slice(1)); + return true; + } + + var rewrittenUrl; var appUrl; - if ( appBase == stripHash(url) ) { - return url; - } else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { - return appBase + hashPrefix + appUrl; - } else if ( appBaseNoFile === url + '/') { - return appBaseNoFile; + if (appBase == stripHash(url)) { + rewrittenUrl = url; + } else if ((appUrl = beginsWith(appBaseNoFile, url))) { + rewrittenUrl = appBase + hashPrefix + appUrl; + } else if (appBaseNoFile === url + '/') { + rewrittenUrl = appBaseNoFile; + } + if (rewrittenUrl) { + this.$$parse(rewrittenUrl); } + return !!rewrittenUrl; + }; + + this.$$compose = function() { + var search = toKeyValue(this.$$search), + hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + + this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#' + this.$$absUrl = appBase + hashPrefix + this.$$url; }; + } -LocationHashbangInHtml5Url.prototype = - LocationHashbangUrl.prototype = - LocationHtml5Url.prototype = { +var locationPrototype = { /** * Are we in html5 mode? @@ -9219,7 +10802,7 @@ LocationHashbangInHtml5Url.prototype = $$html5: false, /** - * Has any change been replacing ? + * Has any change been replacing? * @private */ $$replace: false, @@ -9234,6 +10817,13 @@ LocationHashbangInHtml5Url.prototype = * Return full url representation with all segments encoded according to rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var absUrl = $location.absUrl(); + * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" + * ``` + * * @return {string} full url */ absUrl: locationGetter('$$absUrl'), @@ -9249,18 +10839,24 @@ LocationHashbangInHtml5Url.prototype = * * Change path, search and hash, when called with parameter and return `$location`. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var url = $location.url(); + * // => "/some/path?foo=bar&baz=xoxo" + * ``` + * * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) - * @param {string=} replace The path that will be changed * @return {string} url */ - url: function(url, replace) { + url: function(url) { if (isUndefined(url)) return this.$$url; var match = PATH_MATCH.exec(url); - if (match[1]) this.path(decodeURIComponent(match[1])); - if (match[2] || match[1]) this.search(match[3] || ''); - this.hash(match[5] || '', replace); + if (match[1] || url === '') this.path(decodeURIComponent(match[1])); + if (match[2] || match[1] || url === '') this.search(match[3] || ''); + this.hash(match[5] || ''); return this; }, @@ -9274,6 +10870,13 @@ LocationHashbangInHtml5Url.prototype = * * Return protocol of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var protocol = $location.protocol(); + * // => "http" + * ``` + * * @return {string} protocol of current url */ protocol: locationGetter('$$protocol'), @@ -9287,6 +10890,13 @@ LocationHashbangInHtml5Url.prototype = * * Return host of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var host = $location.host(); + * // => "example.com" + * ``` + * * @return {string} host of current url. */ host: locationGetter('$$host'), @@ -9300,6 +10910,13 @@ LocationHashbangInHtml5Url.prototype = * * Return port of current url. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var port = $location.port(); + * // => 80 + * ``` + * * @return {Number} port */ port: locationGetter('$$port'), @@ -9318,10 +10935,18 @@ LocationHashbangInHtml5Url.prototype = * Note: Path should always begin with forward slash (/), this method will add the forward slash * if it is missing. * - * @param {string=} path New path + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var path = $location.path(); + * // => "/some/path" + * ``` + * + * @param {(string|number)=} path New path * @return {string} path */ path: locationGetterSetter('$$path', function(path) { + path = path !== null ? path.toString() : ''; return path.charAt(0) == '/' ? path : '/' + path; }), @@ -9336,24 +10961,55 @@ LocationHashbangInHtml5Url.prototype = * * Change search part when called with parameter and return `$location`. * + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * var searchObject = $location.search(); + * // => {foo: 'bar', baz: 'xoxo'} + * + * // set foo to 'yipee' + * $location.search('foo', 'yipee'); + * // $location.search() => {foo: 'yipee', baz: 'xoxo'} + * ``` + * * @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or - * hash object. Hash object may contain an array of values, which will be decoded as duplicates in - * the url. + * hash object. + * + * When called with a single argument the method acts as a setter, setting the `search` component + * of `$location` to the specified value. + * + * If the argument is a hash object containing an array of values, these values will be encoded + * as duplicate search parameters in the url. + * + * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` + * will override only a single search property. + * + * If `paramValue` is an array, it will override the property of the `search` component of + * `$location` specified via the first argument. + * + * If `paramValue` is `null`, the property specified via the first argument will be deleted. * - * @param {(string|Array<string>)=} paramValue If `search` is a string, then `paramValue` will override only a - * single search parameter. If `paramValue` is an array, it will set the parameter as a - * comma-separated value. If `paramValue` is `null`, the parameter will be deleted. + * If `paramValue` is `true`, the property specified via the first argument will be added with no + * value nor trailing equal sign. * - * @return {string} search + * @return {Object} If called with no arguments returns the parsed `search` object. If called with + * one or more arguments returns `$location` object itself. */ search: function(search, paramValue) { switch (arguments.length) { case 0: return this.$$search; case 1: - if (isString(search)) { + if (isString(search) || isNumber(search)) { + search = search.toString(); this.$$search = parseKeyValue(search); } else if (isObject(search)) { + search = copy(search, {}); + // remove object undefined or null properties + forEach(search, function(value, key) { + if (value == null) delete search[key]; + }); + this.$$search = search; } else { throw $locationMinErr('isrcharg', @@ -9383,10 +11039,19 @@ LocationHashbangInHtml5Url.prototype = * * Change hash fragment when called with parameter and return `$location`. * - * @param {string=} hash New hash fragment + * + * ```js + * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * var hash = $location.hash(); + * // => "hashValue" + * ``` + * + * @param {(string|number)=} hash New hash fragment * @return {string} hash */ - hash: locationGetterSetter('$$hash', identity), + hash: locationGetterSetter('$$hash', function(hash) { + return hash !== null ? hash.toString() : ''; + }), /** * @ngdoc method @@ -9402,6 +11067,46 @@ LocationHashbangInHtml5Url.prototype = } }; +forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) { + Location.prototype = Object.create(locationPrototype); + + /** + * @ngdoc method + * @name $location#state + * + * @description + * This method is getter / setter. + * + * Return the history state object when called without any parameter. + * + * Change the history state object when called with one parameter and return `$location`. + * The state object is later passed to `pushState` or `replaceState`. + * + * NOTE: This method is supported only in HTML5 mode and only in browsers supporting + * the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support + * older browsers (like IE9 or Android < 4.0), don't use this method. + * + * @param {object=} state State object for pushState or replaceState + * @return {object} state + */ + Location.prototype.state = function(state) { + if (!arguments.length) + return this.$$state; + + if (Location !== LocationHtml5Url || !this.$$html5) { + throw $locationMinErr('nostate', 'History API state support is available only ' + + 'in HTML5 mode and only in browsers supporting HTML5 History API'); + } + // The user might modify `stateObject` after invoking `$location.state(stateObject)` + // but we're changing the $$state reference to $browser.state() during the $digest + // so the modification window is narrow. + this.$$state = isUndefined(state) ? null : state; + + return this; + }; +}); + + function locationGetter(property) { return function() { return this[property]; @@ -9454,12 +11159,16 @@ function locationGetterSetter(property, preprocess) { * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ -function $LocationProvider(){ +function $LocationProvider() { var hashPrefix = '', - html5Mode = false; + html5Mode = { + enabled: false, + requireBase: true, + rewriteLinks: true + }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#hashPrefix * @description * @param {string=} prefix Prefix for hash part (containing path and search) @@ -9475,15 +11184,42 @@ function $LocationProvider(){ }; /** - * @ngdoc property + * @ngdoc method * @name $locationProvider#html5Mode * @description - * @param {boolean=} mode Use HTML5 strategy if available. - * @returns {*} current value if used as getter or itself (chaining) if used as setter + * @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value. + * If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported + * properties: + * - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to + * change urls where supported. Will fall back to hash-prefixed paths in browsers that do not + * support `pushState`. + * - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies + * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are + * true, and a base tag is not present, an error will be thrown when `$location` is injected. + * See the {@link guide/$location $location guide for more information} + * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, + * enables/disables url rewriting for relative links. + * + * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ this.html5Mode = function(mode) { - if (isDefined(mode)) { - html5Mode = mode; + if (isBoolean(mode)) { + html5Mode.enabled = mode; + return this; + } else if (isObject(mode)) { + + if (isBoolean(mode.enabled)) { + html5Mode.enabled = mode.enabled; + } + + if (isBoolean(mode.requireBase)) { + html5Mode.requireBase = mode.requireBase; + } + + if (isBoolean(mode.rewriteLinks)) { + html5Mode.rewriteLinks = mode.rewriteLinks; + } + return this; } else { return html5Mode; @@ -9495,14 +11231,21 @@ function $LocationProvider(){ * @name $location#$locationChangeStart * @eventType broadcast on root scope * @description - * Broadcasted before a URL will change. This change can be prevented by calling + * Broadcasted before a URL will change. + * + * This change can be prevented by calling * `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more * details about event object. Upon successful change - * {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. + * {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired. + * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ /** @@ -9512,20 +11255,29 @@ function $LocationProvider(){ * @description * Broadcasted after a URL was changed. * + * The `newState` and `oldState` parameters may be defined only in HTML5 mode and when + * the browser supports the HTML5 History API. + * * @param {Object} angularEvent Synthetic event object. * @param {string} newUrl New URL * @param {string=} oldUrl URL that was before it was changed. + * @param {string=} newState New history state object + * @param {string=} oldState History state object that was before it was changed. */ - this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', - function( $rootScope, $browser, $sniffer, $rootElement) { + this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window', + function($rootScope, $browser, $sniffer, $rootElement, $window) { var $location, LocationMode, baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' initialUrl = $browser.url(), appBase; - if (html5Mode) { + if (html5Mode.enabled) { + if (!baseHref && html5Mode.requireBase) { + throw $locationMinErr('nobase', + "$location in HTML5 mode requires a <base> tag to be present!"); + } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; } else { @@ -9533,23 +11285,49 @@ function $LocationProvider(){ LocationMode = LocationHashbangUrl; } $location = new LocationMode(appBase, '#' + hashPrefix); - $location.$$parse($location.$$rewrite(initialUrl)); + $location.$$parseLinkUrl(initialUrl, initialUrl); + + $location.$$state = $browser.state(); + + var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + + function setBrowserUrlWithFallback(url, replace, state) { + var oldUrl = $location.url(); + var oldState = $location.$$state; + try { + $browser.url(url, replace, state); + + // Make sure $location.state() returns referentially identical (not just deeply equal) + // state object; this makes possible quick checking if the state changed in the digest + // loop. Checking deep equality would be too expensive. + $location.$$state = $browser.state(); + } catch (e) { + // Restore old values if pushState fails + $location.url(oldUrl); + $location.$$state = oldState; + + throw e; + } + } $rootElement.on('click', function(event) { // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (event.ctrlKey || event.metaKey || event.which == 2) return; + if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return; var elm = jqLite(event.target); // traverse the DOM up to find first A tag - while (lowercase(elm[0].nodeName) !== 'a') { + while (nodeName_(elm[0]) !== 'a') { // ignore rewriting if no A tag (reached root element, or no parent - removed from document) if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } var absHref = elm.prop('href'); + // get the actual href attribute - see + // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx + var relHref = elm.attr('href') || elm.attr('xlink:href'); if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') { // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during @@ -9557,16 +11335,21 @@ function $LocationProvider(){ absHref = urlResolve(absHref.animVal).href; } - var rewrittenUrl = $location.$$rewrite(absHref); + // Ignore when url is started with javascript: or mailto: + if (IGNORE_URI_REGEXP.test(absHref)) return; - if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { - event.preventDefault(); - if (rewrittenUrl != $browser.url()) { + if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) { + if ($location.$$parseLinkUrl(absHref, relHref)) { + // We do a preventDefault for all urls that are part of the angular application, + // in html5mode and also without, so that we are able to abort navigation without + // getting double entries in the location history. + event.preventDefault(); // update location manually - $location.$$parse(rewrittenUrl); - $rootScope.$apply(); - // hack to work around FF6 bug 684208 when scenario runner clicks on links - window.angular['ff-684208-preventDefault'] = true; + if ($location.absUrl() != $browser.url()) { + $rootScope.$apply(); + // hack to work around FF6 bug 684208 when scenario runner clicks on links + $window.angular['ff-684208-preventDefault'] = true; + } } } }); @@ -9577,52 +11360,82 @@ function $LocationProvider(){ $browser.url($location.absUrl(), true); } - // update $location when $browser url changes - $browser.onUrlChange(function(newUrl) { - if ($location.absUrl() != newUrl) { - $rootScope.$evalAsync(function() { - var oldUrl = $location.absUrl(); + var initializing = true; - $location.$$parse(newUrl); - if ($rootScope.$broadcast('$locationChangeStart', newUrl, - oldUrl).defaultPrevented) { - $location.$$parse(oldUrl); - $browser.url(oldUrl); - } else { - afterLocationChange(oldUrl); - } - }); - if (!$rootScope.$$phase) $rootScope.$digest(); - } + // update $location when $browser url changes + $browser.onUrlChange(function(newUrl, newState) { + $rootScope.$evalAsync(function() { + var oldUrl = $location.absUrl(); + var oldState = $location.$$state; + var defaultPrevented; + + $location.$$parse(newUrl); + $location.$$state = newState; + + defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + setBrowserUrlWithFallback(oldUrl, false, oldState); + } else { + initializing = false; + afterLocationChange(oldUrl, oldState); + } + }); + if (!$rootScope.$$phase) $rootScope.$digest(); }); // update browser - var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); + var oldUrl = trimEmptyHash($browser.url()); + var newUrl = trimEmptyHash($location.absUrl()); + var oldState = $browser.state(); var currentReplace = $location.$$replace; + var urlOrStateChanged = oldUrl !== newUrl || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + + if (initializing || urlOrStateChanged) { + initializing = false; - if (!changeCounter || oldUrl != $location.absUrl()) { - changeCounter++; $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). - defaultPrevented) { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; + + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { $location.$$parse(oldUrl); + $location.$$state = oldState; } else { - $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); } }); } + $location.$$replace = false; - return changeCounter; + // we don't need to return anything because $evalAsync will make the digest loop dirty when + // there is a change }); return $location; - function afterLocationChange(oldUrl) { - $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); + function afterLocationChange(oldUrl, oldState) { + $rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, + $location.$$state, oldState); } }]; } @@ -9642,15 +11455,16 @@ function $LocationProvider(){ * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - <example> + <example module="logExample"> <file name="script.js"> - function LogCtrl($scope, $log) { - $scope.$log = $log; - $scope.message = 'Hello World!'; - } + angular.module('logExample', []) + .controller('LogController', ['$scope', '$log', function($scope, $log) { + $scope.$log = $log; + $scope.message = 'Hello World!'; + }]); </file> <file name="index.html"> - <div ng-controller="LogCtrl"> + <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"/> @@ -9669,12 +11483,12 @@ function $LocationProvider(){ * @description * Use the `$logProvider` to configure how the application logs messages */ -function $LogProvider(){ +function $LogProvider() { var debug = true, self = this; /** - * @ngdoc property + * @ngdoc method * @name $logProvider#debugEnabled * @description * @param {boolean=} flag enable or disable debug level messages @@ -9689,7 +11503,7 @@ function $LogProvider(){ } }; - this.$get = ['$window', function($window){ + this.$get = ['$window', function($window) { return { /** * @ngdoc method @@ -9734,7 +11548,7 @@ function $LogProvider(){ * @description * Write a debug message */ - debug: (function () { + debug: (function() { var fn = consoleLog('debug'); return function() { @@ -9789,43 +11603,38 @@ function $LogProvider(){ } var $parseMinErr = minErr('$parse'); -var promiseWarningCache = {}; -var promiseWarning; // Sandboxing Angular Expressions // ------------------------------ // Angular expressions are generally considered safe because these expressions only have direct -// access to $scope and locals. However, one can obtain the ability to execute arbitrary JS code by +// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by // obtaining a reference to native JS functions such as the Function constructor. // // As an example, consider the following Angular expression: // -// {}.toString.constructor(alert("evil JS code")) -// -// We want to prevent this type of access. For the sake of performance, during the lexing phase we -// disallow any "dotted" access to any member named "constructor". -// -// For reflective calls (a[b]) we check that the value of the lookup is not the Function constructor -// while evaluating the expression, which is a stronger but more expensive test. Since reflective -// calls are expensive anyway, this is not such a big deal compared to static dereferencing. +// {}.toString.constructor('alert("evil JS code")') // // This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits // against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser apis on Scope. Exposing such objects on a Scope is never a good +// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good // practice and therefore we are not even trying to protect against interaction with an object // explicitly exposed in this way. // -// A developer could foil the name check by aliasing the Function constructor under a different -// name on the scope. -// // In general, it is not possible to access a Window object from an angular expression unless a // window or some DOM object that has a reference to window is published onto a Scope. +// Similarly we prevent invocations of function known to be dangerous, as well as assignments to +// native objects. +// +// See https://docs.angularjs.org/guide/security + function ensureSafeMemberName(name, fullExpression) { - if (name === "constructor") { + if (name === "__defineGetter__" || name === "__defineSetter__" + || name === "__lookupGetter__" || name === "__lookupSetter__" + || name === "__proto__") { throw $parseMinErr('isecfld', - 'Referencing "constructor" field in Angular expressions is disallowed! Expression: {0}', - fullExpression); + 'Attempting to access a disallowed field in Angular expressions! ' + + 'Expression: {0}', fullExpression); } return name; } @@ -9838,7 +11647,7 @@ function ensureSafeObject(obj, fullExpression) { 'Referencing Function in Angular expressions is disallowed! Expression: {0}', fullExpression); } else if (// isWindow(obj) - obj.document && obj.location && obj.alert && obj.setInterval) { + obj.window === obj) { throw $parseMinErr('isecwindow', 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', fullExpression); @@ -9847,18 +11656,54 @@ function ensureSafeObject(obj, fullExpression) { throw $parseMinErr('isecdom', 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression); + } else if (// block Object so that we can't get hold of dangerous Object.* methods + obj === Object) { + throw $parseMinErr('isecobj', + 'Referencing Object in Angular expressions is disallowed! Expression: {0}', + fullExpression); } } return obj; } -var OPERATORS = { - /* jshint bitwise : false */ - 'null':function(){return null;}, - 'true':function(){return true;}, - 'false':function(){return false;}, - undefined:noop, - '+':function(self, locals, a,b){ +var CALL = Function.prototype.call; +var APPLY = Function.prototype.apply; +var BIND = Function.prototype.bind; + +function ensureSafeFunction(obj, fullExpression) { + if (obj) { + if (obj.constructor === obj) { + throw $parseMinErr('isecfn', + 'Referencing Function in Angular expressions is disallowed! Expression: {0}', + fullExpression); + } else if (obj === CALL || obj === APPLY || obj === BIND) { + throw $parseMinErr('isecff', + 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', + 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; + + +//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)) { @@ -9866,32 +11711,30 @@ var OPERATORS = { } return a; } - return isDefined(b)?b:undefined;}, - '-':function(self, locals, a,b){ + 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); + 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);}, - '=':noop, - '===':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|b;}, - '|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));}, - '!':function(self, locals, a){return !a(self, locals);} -}; -/* jshint bitwise: true */ + '*':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 ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; @@ -9901,85 +11744,51 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function (options) { +var Lexer = function(options) { this.options = options; }; Lexer.prototype = { constructor: Lexer, - lex: function (text) { + lex: function(text) { this.text = text; - this.index = 0; - this.ch = undefined; - this.lastCh = ':'; // can start regexp - this.tokens = []; - var token; - var json = []; - while (this.index < this.text.length) { - this.ch = this.text.charAt(this.index); - if (this.is('"\'')) { - this.readString(this.ch); - } else if (this.isNumber(this.ch) || this.is('.') && this.isNumber(this.peek())) { + var ch = this.text.charAt(this.index); + if (ch === '"' || ch === "'") { + this.readString(ch); + } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(this.ch)) { + } else if (this.isIdent(ch)) { this.readIdent(); - // identifiers can only be if the preceding char was a { or , - if (this.was('{,') && json[0] === '{' && - (token = this.tokens[this.tokens.length - 1])) { - token.json = token.text.indexOf('.') === -1; - } - } else if (this.is('(){}[].,;:?')) { - this.tokens.push({ - index: this.index, - text: this.ch, - json: (this.was(':[,') && this.is('{[')) || this.is('}]:,') - }); - if (this.is('{[')) json.unshift(this.ch); - if (this.is('}]')) json.shift(); + } else if (this.is(ch, '(){}[].,;:?')) { + this.tokens.push({index: this.index, text: ch}); this.index++; - } else if (this.isWhitespace(this.ch)) { + } else if (this.isWhitespace(ch)) { this.index++; - continue; } else { - var ch2 = this.ch + this.peek(); + var ch2 = ch + this.peek(); var ch3 = ch2 + this.peek(2); - var fn = OPERATORS[this.ch]; - var fn2 = OPERATORS[ch2]; - var fn3 = OPERATORS[ch3]; - if (fn3) { - this.tokens.push({index: this.index, text: ch3, fn: fn3}); - this.index += 3; - } else if (fn2) { - this.tokens.push({index: this.index, text: ch2, fn: fn2}); - this.index += 2; - } else if (fn) { - this.tokens.push({ - index: this.index, - text: this.ch, - fn: fn, - json: (this.was('[,:') && this.is('+-')) - }); - this.index += 1; + var op1 = OPERATORS[ch]; + var op2 = OPERATORS[ch2]; + var op3 = OPERATORS[ch3]; + if (op1 || op2 || op3) { + var token = op3 ? ch3 : (op2 ? ch2 : ch); + this.tokens.push({index: this.index, text: token, operator: true}); + this.index += token.length; } else { this.throwError('Unexpected next character ', this.index, this.index + 1); } } - this.lastCh = this.ch; } return this.tokens; }, - is: function(chars) { - return chars.indexOf(this.ch) !== -1; - }, - - was: function(chars) { - return chars.indexOf(this.lastCh) !== -1; + is: function(ch, chars) { + return chars.indexOf(ch) !== -1; }, peek: function(i) { @@ -9988,7 +11797,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9'); + return ('0' <= ch && ch <= '9') && typeof ch === "string"; }, isWhitespace: function(ch) { @@ -10041,88 +11850,28 @@ Lexer.prototype = { } this.index++; } - number = 1 * number; this.tokens.push({ index: start, text: number, - json: true, - fn: function() { return number; } + constant: true, + value: Number(number) }); }, readIdent: function() { - var parser = this; - - var ident = ''; var start = this.index; - - var lastDot, peekIndex, methodName, ch; - while (this.index < this.text.length) { - ch = this.text.charAt(this.index); - if (ch === '.' || this.isIdent(ch) || this.isNumber(ch)) { - if (ch === '.') lastDot = this.index; - ident += ch; - } else { + var ch = this.text.charAt(this.index); + if (!(this.isIdent(ch) || this.isNumber(ch))) { break; } this.index++; } - - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = this.index; - while (peekIndex < this.text.length) { - ch = this.text.charAt(peekIndex); - if (ch === '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - this.index = peekIndex; - break; - } - if (this.isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } - } - - - var token = { + this.tokens.push({ index: start, - text: ident - }; - - // OPERATORS is our own object so we don't need to use special hasOwnPropertyFn - if (OPERATORS.hasOwnProperty(ident)) { - token.fn = OPERATORS[ident]; - token.json = OPERATORS[ident]; - } else { - var getter = getterFn(ident, this.options, this.text); - token.fn = extend(function(self, locals) { - return (getter(self, locals)); - }, { - assign: function(self, value) { - return setter(self, ident, value, parser.text, parser.options); - } - }); - } - - this.tokens.push(token); - - if (methodName) { - this.tokens.push({ - index:lastDot, - text: '.', - json: false - }); - this.tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } + text: this.text.slice(start, this.index), + identifier: true + }); }, readString: function(quote) { @@ -10143,11 +11892,7 @@ Lexer.prototype = { string += String.fromCharCode(parseInt(hex, 16)); } else { var rep = ESCAPE[ch]; - if (rep) { - string += rep; - } else { - string += ch; - } + string = string + (rep || ch); } escape = false; } else if (ch === '\\') { @@ -10157,9 +11902,8 @@ Lexer.prototype = { this.tokens.push({ index: start, text: rawString, - string: string, - json: true, - fn: function() { return string; } + constant: true, + value: string }); return; } else { @@ -10172,46 +11916,34 @@ Lexer.prototype = { }; +function isConstant(exp) { + return exp.constant; +} + /** * @constructor */ -var Parser = function (lexer, $filter, options) { +var Parser = function(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; }; -Parser.ZERO = extend(function () { +Parser.ZERO = extend(function() { return 0; }, { + sharedGetter: true, constant: true }); Parser.prototype = { constructor: Parser, - parse: function (text, json) { + parse: function(text) { this.text = text; - - //TODO(i): strip all the obsolte json stuff from this file - this.json = json; - this.tokens = this.lexer.lex(text); - if (json) { - // The extra level of aliasing is here, just in case the lexer misses something, so that - // we prevent any accidental execution in JSON. - this.assignment = this.logicalOR; - - this.functionCall = - this.fieldAccess = - this.objectIndex = - this.filterChain = function() { - this.throwError('is not valid json', {text: text, index: 0}); - }; - } - - var value = json ? this.primary() : this.statements(); + var value = this.statements(); if (this.tokens.length !== 0) { this.throwError('is an unexpected token', this.tokens[0]); @@ -10223,7 +11955,7 @@ Parser.prototype = { return value; }, - primary: function () { + primary: function() { var primary; if (this.expect('(')) { primary = this.filterChain(); @@ -10232,16 +11964,14 @@ Parser.prototype = { 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 { - var token = this.expect(); - primary = token.fn; - if (!primary) { - this.throwError('not a primary expression', token); - } - if (token.json) { - primary.constant = true; - primary.literal = true; - } + this.throwError('not a primary expression', this.peek()); } var next, context; @@ -10275,8 +12005,11 @@ Parser.prototype = { }, peek: function(e1, e2, e3, e4) { - if (this.tokens.length > 0) { - var token = this.tokens[0]; + 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)) { @@ -10286,45 +12019,66 @@ Parser.prototype = { return false; }, - expect: function(e1, e2, e3, e4){ + expect: function(e1, e2, e3, e4) { var token = this.peek(e1, e2, e3, e4); if (token) { - if (this.json && !token.json) { - this.throwError('is not valid json', token); - } this.tokens.shift(); return token; } return false; }, - consume: function(e1){ - if (!this.expect(e1)) { + 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(fn, right) { - return extend(function(self, locals) { + unaryFn: function(op, right) { + var fn = OPERATORS[op]; + return extend(function $parseUnaryFn(self, locals) { return fn(self, locals, right); }, { - constant:right.constant + constant:right.constant, + inputs: [right] }); }, - ternaryFn: function(left, middle, right){ - return extend(function(self, locals){ - return left(self, locals) ? middle(self, locals) : right(self, locals); + 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 && middle.constant && right.constant + constant: left.constant && right.constant, + inputs: !isBranching && [left, right] }); }, - binaryFn: function(left, fn, right) { - return extend(function(self, locals) { - return fn(self, locals, 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:left.constant && right.constant + constant: true, + literal: true }); }, @@ -10338,13 +12092,10 @@ Parser.prototype = { // TODO(size): maybe we should not support multiple statements? return (statements.length === 1) ? statements[0] - : function(self, locals) { + : function $parseStatements(self, locals) { var value; - for (var i = 0; i < statements.length; i++) { - var statement = statements[i]; - if (statement) { - value = statement(self, locals); - } + for (var i = 0, ii = statements.length; i < ii; i++) { + value = statements[i](self, locals); } return value; }; @@ -10355,35 +12106,45 @@ Parser.prototype = { filterChain: function() { var left = this.expression(); var token; - while (true) { - if ((token = this.expect('|'))) { - left = this.binaryFn(left, token.fn, this.filter()); - } else { - return left; - } + while ((token = this.expect('|'))) { + left = this.filter(left); } + return left; }, - filter: function() { - var token = this.expect(); - var fn = this.$filter(token.text); - var argsFn = []; - while (true) { - if ((token = this.expect(':'))) { + 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()); - } else { - var fnInvoke = function(self, locals, input) { - var args = [input]; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](self, locals)); - } - return fn.apply(self, args); - }; - return function() { - return fnInvoke; - }; } } + + 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() { @@ -10400,9 +12161,11 @@ Parser.prototype = { this.text.substring(0, token.index) + '] can not be assigned to', token); } right = this.ternary(); - return function(scope, locals) { + return extend(function $parseAssignment(scope, locals) { return left.assign(scope, right(scope, locals), locals); - }; + }, { + inputs: [left, right] + }); } return left; }, @@ -10412,34 +12175,35 @@ Parser.prototype = { var middle; var token; if ((token = this.expect('?'))) { - middle = this.ternary(); - if ((token = this.expect(':'))) { - return this.ternaryFn(left, middle, this.ternary()); - } else { - this.throwError('expected :', token); + middle = this.assignment(); + 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 + }); } - } else { - return left; } + + return left; }, logicalOR: function() { var left = this.logicalAND(); var token; - while (true) { - if ((token = this.expect('||'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); - } else { - return left; - } + while ((token = this.expect('||'))) { + left = this.binaryFn(left, token.text, this.logicalAND(), true); } + return left; }, logicalAND: function() { var left = this.equality(); var token; - if ((token = this.expect('&&'))) { - left = this.binaryFn(left, token.fn, this.logicalAND()); + while ((token = this.expect('&&'))) { + left = this.binaryFn(left, token.text, this.equality(), true); } return left; }, @@ -10447,8 +12211,8 @@ Parser.prototype = { equality: function() { var left = this.relational(); var token; - if ((token = this.expect('==','!=','===','!=='))) { - left = this.binaryFn(left, token.fn, this.equality()); + while ((token = this.expect('==','!=','===','!=='))) { + left = this.binaryFn(left, token.text, this.relational()); } return left; }, @@ -10456,8 +12220,8 @@ Parser.prototype = { relational: function() { var left = this.additive(); var token; - if ((token = this.expect('<', '>', '<=', '>='))) { - left = this.binaryFn(left, token.fn, this.relational()); + while ((token = this.expect('<', '>', '<=', '>='))) { + left = this.binaryFn(left, token.text, this.additive()); } return left; }, @@ -10466,7 +12230,7 @@ Parser.prototype = { var left = this.multiplicative(); var token; while ((token = this.expect('+','-'))) { - left = this.binaryFn(left, token.fn, this.multiplicative()); + left = this.binaryFn(left, token.text, this.multiplicative()); } return left; }, @@ -10475,7 +12239,7 @@ Parser.prototype = { var left = this.unary(); var token; while ((token = this.expect('*','/','%'))) { - left = this.binaryFn(left, token.fn, this.unary()); + left = this.binaryFn(left, token.text, this.unary()); } return left; }, @@ -10485,61 +12249,56 @@ Parser.prototype = { if (this.expect('+')) { return this.primary(); } else if ((token = this.expect('-'))) { - return this.binaryFn(Parser.ZERO, token.fn, this.unary()); + return this.binaryFn(Parser.ZERO, token.text, this.unary()); } else if ((token = this.expect('!'))) { - return this.unaryFn(token.fn, this.unary()); + return this.unaryFn(token.text, this.unary()); } else { return this.primary(); } }, fieldAccess: function(object) { - var parser = this; - var field = this.expect().text; - var getter = getterFn(field, this.options, this.text); + var getter = this.identifier(); - return extend(function(scope, locals, self) { - return getter(self || object(scope, locals)); + return extend(function $parseFieldAccess(scope, locals, self) { + var o = self || object(scope, locals); + return (o == null) ? undefined : getter(o); }, { assign: function(scope, value, locals) { - return setter(object(scope, locals), field, value, parser.text, parser.options); + var o = object(scope, locals); + if (!o) object.assign(scope, o = {}); + return getter.assign(o, value); } }); }, objectIndex: function(obj) { - var parser = this; + var expression = this.text; var indexFn = this.expression(); this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseObjectIndex(self, locals) { var o = obj(self, locals), i = indexFn(self, locals), - v, p; + v; + ensureSafeMemberName(i, expression); if (!o) return undefined; - v = ensureSafeObject(o[i], parser.text); - if (v && v.then && parser.options.unwrapPromises) { - p = v; - if (!('$$v' in v)) { - p.$$v = undefined; - p.then(function(val) { p.$$v = val; }); - } - v = v.$$v; - } + v = ensureSafeObject(o[i], expression); return v; }, { assign: function(self, value, locals) { - var key = indexFn(self, locals); + var key = ensureSafeMemberName(indexFn(self, locals), expression); // prevent overwriting of Function.constructor which would break ensureSafeObject check - var safe = ensureSafeObject(obj(self, locals), parser.text); - return safe[key] = value; + var o = ensureSafeObject(obj(self, locals), expression); + if (!o) obj.assign(self, o = {}); + return o[key] = value; } }); }, - functionCall: function(fn, contextGetter) { + functionCall: function(fnGetter, contextGetter) { var argsFn = []; if (this.peekToken().text !== ')') { do { @@ -10548,91 +12307,92 @@ Parser.prototype = { } this.consume(')'); - var parser = this; + var expressionText = this.text; + // we can safely reuse the array across invocations + var args = argsFn.length ? [] : null; - return function(scope, locals) { - var args = []; - var context = contextGetter ? contextGetter(scope, locals) : scope; + return function $parseFunctionCall(scope, locals) { + var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope; + var fn = fnGetter(scope, locals, context) || noop; - for (var i = 0; i < argsFn.length; i++) { - args.push(argsFn[i](scope, locals)); + if (args) { + var i = argsFn.length; + while (i--) { + args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText); + } } - var fnPtr = fn(scope, locals, context) || noop; - ensureSafeObject(context, parser.text); - ensureSafeObject(fnPtr, parser.text); + ensureSafeObject(context, expressionText); + ensureSafeFunction(fn, expressionText); - // IE stupidity! (IE doesn't have apply for some native functions) - var v = fnPtr.apply - ? fnPtr.apply(context, args) - : fnPtr(args[0], args[1], args[2], args[3], args[4]); + // 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 ensureSafeObject(v, parser.text); - }; + return ensureSafeObject(v, expressionText); + }; }, // This is used with json array declaration - arrayDeclaration: function () { + arrayDeclaration: function() { var elementFns = []; - var allConstant = true; if (this.peekToken().text !== ']') { do { if (this.peek(']')) { // Support trailing commas per ES5.1. break; } - var elementFn = this.expression(); - elementFns.push(elementFn); - if (!elementFn.constant) { - allConstant = false; - } + elementFns.push(this.expression()); } while (this.expect(',')); } this.consume(']'); - return extend(function(self, locals) { + return extend(function $parseArrayLiteral(self, locals) { var array = []; - for (var i = 0; i < elementFns.length; i++) { + for (var i = 0, ii = elementFns.length; i < ii; i++) { array.push(elementFns[i](self, locals)); } return array; }, { literal: true, - constant: allConstant + constant: elementFns.every(isConstant), + inputs: elementFns }); }, - object: function () { - var keyValues = []; - var allConstant = true; + object: function() { + var keys = [], valueFns = []; if (this.peekToken().text !== '}') { do { if (this.peek('}')) { // Support trailing commas per ES5.1. break; } - var token = this.expect(), - key = token.string || token.text; - this.consume(':'); - var value = this.expression(); - keyValues.push({key: key, value: value}); - if (!value.constant) { - allConstant = false; + var token = this.consume(); + if (token.constant) { + keys.push(token.value); + } else if (token.identifier) { + keys.push(token.text); + } else { + this.throwError("invalid key", token); } + this.consume(':'); + valueFns.push(this.expression()); } while (this.expect(',')); } this.consume('}'); - return extend(function(self, locals) { + return extend(function $parseObjectLiteral(self, locals) { var object = {}; - for (var i = 0; i < keyValues.length; i++) { - var keyValue = keyValues[i]; - object[keyValue.key] = keyValue.value(self, locals); + for (var i = 0, ii = valueFns.length; i < ii; i++) { + object[keys[i]] = valueFns[i](self, locals); } return object; }, { literal: true, - constant: allConstant + constant: valueFns.every(isConstant), + inputs: valueFns }); } }; @@ -10642,196 +12402,104 @@ Parser.prototype = { // Parser helper functions ////////////////////////////////////////////////// -function setter(obj, path, setValue, fullExp, options) { - //needed? - options = options || {}; +function setter(obj, path, setValue, fullExp) { + ensureSafeObject(obj, fullExp); var element = path.split('.'), key; for (var i = 0; element.length > 1; i++) { key = ensureSafeMemberName(element.shift(), fullExp); - var propertyObj = obj[key]; + var propertyObj = ensureSafeObject(obj[key], fullExp); if (!propertyObj) { propertyObj = {}; obj[key] = propertyObj; } obj = propertyObj; - if (obj.then && options.unwrapPromises) { - promiseWarning(fullExp); - if (!("$$v" in obj)) { - (function(promise) { - promise.then(function(val) { promise.$$v = val; }); } - )(obj); - } - if (obj.$$v === undefined) { - obj.$$v = {}; - } - obj = obj.$$v; - } } key = ensureSafeMemberName(element.shift(), fullExp); + ensureSafeObject(obj[key], fullExp); obj[key] = setValue; return setValue; } -var getterFnCache = {}; +var getterFnCacheDefault = createMap(); +var getterFnCacheExpensive = createMap(); + +function isPossiblyDangerousMemberName(name) { + return name == 'constructor'; +} /** * 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, options) { +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; - return !options.unwrapPromises - ? function cspSafeGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - - if (pathVal == null) return pathVal; - pathVal = pathVal[key0]; - - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - - return pathVal; - } - : function cspSafePromiseEnabledGetter(scope, locals) { - var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope, - promise; - - if (pathVal == null) return pathVal; - - pathVal = pathVal[key0]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + return function cspSafeGetter(scope, locals) { + var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope; - if (!key1) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key1]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (pathVal == null) return pathVal; + pathVal = eso0(pathVal[key0]); - if (!key2) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key2]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key1) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso1(pathVal[key1]); - if (!key3) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key3]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } + if (!key2) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso2(pathVal[key2]); - if (!key4) return pathVal; - if (pathVal == null) return undefined; - pathVal = pathVal[key4]; - if (pathVal && pathVal.then) { - promiseWarning(fullExp); - if (!("$$v" in pathVal)) { - promise = pathVal; - promise.$$v = undefined; - promise.then(function(val) { promise.$$v = val; }); - } - pathVal = pathVal.$$v; - } - return pathVal; - }; -} + if (!key3) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso3(pathVal[key3]); -function simpleGetterFn1(key0, fullExp) { - ensureSafeMemberName(key0, fullExp); + if (!key4) return pathVal; + if (pathVal == null) return undefined; + pathVal = eso4(pathVal[key4]); - return function simpleGetterFn1(scope, locals) { - if (scope == null) return undefined; - return ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; + return pathVal; }; } -function simpleGetterFn2(key0, key1, fullExp) { - ensureSafeMemberName(key0, fullExp); - ensureSafeMemberName(key1, fullExp); - - return function simpleGetterFn2(scope, locals) { - if (scope == null) return undefined; - scope = ((locals && locals.hasOwnProperty(key0)) ? locals : scope)[key0]; - return scope == null ? undefined : scope[key1]; +function getterFnWithEnsureSafeObject(fn, fullExpression) { + return function(s, l) { + return fn(s, l, ensureSafeObject, fullExpression); }; } function getterFn(path, options, fullExp) { - // Check whether the cache has this getter already. - // We can use hasOwnProperty directly on the cache because we ensure, - // see below, that the cache never stores a path called 'hasOwnProperty' - if (getterFnCache.hasOwnProperty(path)) { - return getterFnCache[path]; - } + var expensiveChecks = options.expensiveChecks; + var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault); + var fn = getterFnCache[path]; + if (fn) return fn; + var pathKeys = path.split('.'), - pathKeysLength = pathKeys.length, - fn; + pathKeysLength = pathKeys.length; - // When we have only 1 or 2 tokens, use optimized special case closures. // http://jsperf.com/angularjs-parse-getter/6 - if (!options.unwrapPromises && pathKeysLength === 1) { - fn = simpleGetterFn1(pathKeys[0], fullExp); - } else if (!options.unwrapPromises && pathKeysLength === 2) { - fn = simpleGetterFn2(pathKeys[0], pathKeys[1], fullExp); - } else if (options.csp) { + if (options.csp) { if (pathKeysLength < 6) { - fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, - options); + fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks); } else { - fn = function(scope, locals) { + fn = function cspSafeGetter(scope, locals) { var i = 0, val; do { val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], - pathKeys[i++], fullExp, options)(scope, locals); + pathKeys[i++], fullExp, expensiveChecks)(scope, locals); locals = undefined; // clear after first iteration scope = val; @@ -10840,46 +12508,51 @@ function getterFn(path, options, fullExp) { }; } } else { - var code = 'var p;\n'; + 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); - code += 'if(s == null) return undefined;\n' + - 's='+ (index + 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 - : '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' + - (options.unwrapPromises - ? 'if (s && s.then) {\n' + - ' pw("' + fullExp.replace(/(["\r\n])/g, '\\$1') + '");\n' + - ' if (!("$$v" in s)) {\n' + - ' p=s;\n' + - ' p.$$v = undefined;\n' + - ' p.then(function(v) {p.$$v=v;});\n' + - '}\n' + - ' s=s.$$v\n' + - '}\n' - : ''); + : '((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'; }); code += 'return s;'; /* jshint -W054 */ - var evaledFnGetter = new Function('s', 'k', 'pw', code); // s=scope, k=locals, pw=promiseWarning + var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject /* jshint +W054 */ evaledFnGetter.toString = valueFn(code); - fn = options.unwrapPromises ? function(scope, locals) { - return evaledFnGetter(scope, locals, promiseWarning); - } : evaledFnGetter; + if (needsEnsureSafeObject) { + evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp); + } + fn = evaledFnGetter; } - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - if (path !== 'hasOwnProperty') { - getterFnCache[path] = fn; - } + fn.sharedGetter = true; + fn.assign = function(self, value) { + return setter(self, path, value, path); + }; + getterFnCache[path] = fn; return fn; } +var objectValueOf = Object.prototype.valueOf; + +function getValueOf(value) { + return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); +} + /////////////////////////////////// /** @@ -10926,142 +12599,257 @@ function getterFn(path, options, fullExp) { /** * @ngdoc provider * @name $parseProvider - * @function * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cache = {}; + var cacheDefault = createMap(); + var cacheExpensive = createMap(); - var $parseOptions = { - csp: false, - unwrapPromises: false, - logPromiseWarnings: true - }; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#unwrapPromises - * @description - * - * **This feature is deprecated, see deprecation notes below for more info** - * - * If set to true (default is false), $parse will unwrap promises automatically when a promise is - * found at any part of the expression. In other words, if set to true, the expression will always - * result in a non-promise value. - * - * While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, - * the fulfillment value is used in place of the promise while evaluating the expression. - * - * **Deprecation notice** - * - * This is a feature that didn't prove to be wildly useful or popular, primarily because of the - * dichotomy between data access in templates (accessed as raw values) and controller code - * (accessed as promises). - * - * In most code we ended up resolving promises manually in controllers anyway and thus unifying - * the model access there. - * - * Other downsides of automatic promise unwrapping: - * - * - when building components it's often desirable to receive the raw promises - * - adds complexity and slows down expression evaluation - * - makes expression code pre-generation unattractive due to the amount of code that needs to be - * generated - * - makes IDE auto-completion and tool support hard - * - * **Warning Logs** - * - * If the unwrapping is enabled, Angular will log a warning about each expression that unwraps a - * promise (to reduce the noise, each expression is logged only once). To disable this logging use - * `$parseProvider.logPromiseWarnings(false)` api. - * - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.unwrapPromises = function(value) { - if (isDefined(value)) { - $parseOptions.unwrapPromises = !!value; - return this; - } else { - return $parseOptions.unwrapPromises; - } - }; + this.$get = ['$filter', '$sniffer', function($filter, $sniffer) { + var $parseOptions = { + csp: $sniffer.csp, + expensiveChecks: false + }, + $parseOptionsExpensive = { + csp: $sniffer.csp, + expensiveChecks: true + }; + function wrapSharedExpression(exp) { + var wrapped = exp; - /** - * @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future. - * - * @ngdoc method - * @name $parseProvider#logPromiseWarnings - * @description - * - * Controls whether Angular should log a warning on any encounter of a promise in an expression. - * - * The default is set to `true`. - * - * This setting applies only if `$parseProvider.unwrapPromises` setting is set to true as well. - * - * @param {boolean=} value New value. - * @returns {boolean|self} Returns the current setting when used as getter and self if used as - * setter. - */ - this.logPromiseWarnings = function(value) { - if (isDefined(value)) { - $parseOptions.logPromiseWarnings = value; - return this; - } else { - return $parseOptions.logPromiseWarnings; + if (exp.sharedGetter) { + wrapped = function $parseWrapper(self, locals) { + return exp(self, locals); + }; + wrapped.literal = exp.literal; + wrapped.constant = exp.constant; + wrapped.assign = exp.assign; + } + + return wrapped; } - }; + return function $parse(exp, interceptorFn, expensiveChecks) { + var parsedExpression, oneTime, cacheKey; - this.$get = ['$filter', '$sniffer', '$log', function($filter, $sniffer, $log) { - $parseOptions.csp = $sniffer.csp; + switch (typeof exp) { + case 'string': + cacheKey = exp = exp.trim(); - promiseWarning = function promiseWarningFn(fullExp) { - if (!$parseOptions.logPromiseWarnings || promiseWarningCache.hasOwnProperty(fullExp)) return; - promiseWarningCache[fullExp] = true; - $log.warn('[$parse] Promise found in the expression `' + fullExp + '`. ' + - 'Automatic unwrapping of promises in Angular expressions is deprecated.'); + var cache = (expensiveChecks ? cacheExpensive : cacheDefault); + parsedExpression = cache[cacheKey]; + + if (!parsedExpression) { + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + 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; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; + } + + cache[cacheKey] = parsedExpression; + } + return addInterceptor(parsedExpression, interceptorFn); + + case 'function': + return addInterceptor(exp, interceptorFn); + + default: + return addInterceptor(noop, interceptorFn); + } }; - return function(exp) { - var parsedExpression; + 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); + } + } + } - switch (typeof exp) { - case 'string': + return list; + } + + function expressionInputDirtyCheck(newValue, oldValueOfValue) { + + if (newValue == null || oldValueOfValue == null) { // null/undefined + return newValue === oldValueOfValue; + } + + if (typeof newValue === 'object') { + + // attempt to convert the value to a primitive type + // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can + // be cheaply dirty-checked + newValue = getValueOf(newValue); + + if (typeof newValue === 'object') { + // objects/arrays are not supported - deep-watching them would be too expensive + return false; + } + + // fall-through to the primitive equality check + } - if (cache.hasOwnProperty(exp)) { - return cache[exp]; + //Primitive or NaN + return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); + } + + function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var inputExpressions = parsedExpression.$$inputs || + (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, [])); + + var lastResult; + + if (inputExpressions.length === 1) { + var oldInputValue = 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); } + return lastResult; + }, listener, objectEquality); + } + + var oldInputValueOfValues = []; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails + } - var lexer = new Lexer($parseOptions); - var parser = new Parser(lexer, $filter, $parseOptions); - parsedExpression = parser.parse(exp, false); + return scope.$watch(function expressionInputsWatch(scope) { + var changed = false; - if (exp !== 'hasOwnProperty') { - // Only cache the value if it's not going to mess up the cache object - // This is more performant that using Object.prototype.hasOwnProperty.call - cache[exp] = parsedExpression; + for (var i = 0, ii = inputExpressions.length; i < ii; i++) { + var newInputValue = inputExpressions[i](scope); + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } + } - return parsedExpression; + if (changed) { + lastResult = parsedExpression(scope); + } - case 'function': - return exp; + return lastResult; + }, listener, objectEquality); + } - default: - return noop; + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.apply(this, arguments); + } + if (isDefined(value)) { + scope.$$postDigest(function() { + if (isDefined(lastValue)) { + unwatch(); + } + }); + } + }, objectEquality); + } + + function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch, lastValue; + return unwatch = scope.$watch(function oneTimeWatch(scope) { + return parsedExpression(scope); + }, function oneTimeListener(value, old, scope) { + lastValue = value; + if (isFunction(listener)) { + listener.call(this, value, old, scope); + } + if (isAllDefined(value)) { + scope.$$postDigest(function() { + if (isAllDefined(lastValue)) unwatch(); + }); + } + }, objectEquality); + + function isAllDefined(value) { + var allDefined = true; + forEach(value, function(val) { + if (!isDefined(val)) allDefined = false; + }); + return allDefined; } - }; + } + + function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { + var unwatch; + return unwatch = scope.$watch(function constantWatch(scope) { + return parsedExpression(scope); + }, function constantListener(value, old, scope) { + if (isFunction(listener)) { + listener.apply(this, arguments); + } + unwatch(); + }, objectEquality); + } + + function addInterceptor(parsedExpression, interceptorFn) { + if (!interceptorFn) return parsedExpression; + var watchDelegate = parsedExpression.$$watchDelegate; + + var regularWatch = + watchDelegate !== oneTimeLiteralWatchDelegate && + watchDelegate !== oneTimeWatchDelegate; + + var fn = regularWatch ? function regularInterceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + return interceptorFn(value, scope, locals); + } : function oneTimeInterceptedExpression(scope, locals) { + var value = parsedExpression(scope, locals); + var result = interceptorFn(value, scope, locals); + // we only return the interceptor's result if the + // initial value is defined (for bind-once) + return isDefined(value) ? result : value; + }; + + // Propagate $$watchDelegates other then inputsWatchDelegate + if (parsedExpression.$$watchDelegate && + parsedExpression.$$watchDelegate !== inputsWatchDelegate) { + fn.$$watchDelegate = parsedExpression.$$watchDelegate; + } else if (!interceptorFn.$stateful) { + // 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]; + } + + return fn; + } }]; } @@ -11071,7 +12859,54 @@ function $ParseProvider() { * @requires $rootScope * * @description - * A promise/deferred implementation inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). + * A service that helps you run functions asynchronously, and use their return values (or exceptions) + * when they are done processing. + * + * This is an implementation of promises/deferred objects inspired by + * [Kris Kowal's Q](https://github.com/kriskowal/q). + * + * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred + * implementations, and the other which resembles ES6 promises to some degree. + * + * # $q constructor + * + * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` + * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + * + * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * available yet. + * + * It can be used like so: + * + * ```js + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` + * // are available in the current lexical scope (they could have been injected or passed in). + * + * function asyncGreet(name) { + * // perform some asynchronous operation, resolve or reject the promise when appropriate. + * return $q(function(resolve, reject) { + * setTimeout(function() { + * if (okToGreet(name)) { + * resolve('Hello, ' + name + '!'); + * } else { + * reject('Greeting ' + name + ' is not allowed.'); + * } + * }, 1000); + * }); + * } + * + * var promise = asyncGreet('Robin Hood'); + * promise.then(function(greeting) { + * alert('Success: ' + greeting); + * }, function(reason) { + * alert('Failed: ' + reason); + * }); + * ``` + * + * Note: progress/notify callbacks are not currently supported via the ES6-style interface. + * + * 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 * interface for interacting with an object that represents the result of an action that is @@ -11081,24 +12916,20 @@ function $ParseProvider() { * asynchronous programming what `try`, `catch` and `throw` keywords are to synchronous programming. * * ```js - * // for the purpose of this example let's assume that variables `$q`, `scope` and `okToGreet` + * // for the purpose of this example let's assume that variables `$q` and `okToGreet` * // are available in the current lexical scope (they could have been injected or passed in). * * function asyncGreet(name) { * var deferred = $q.defer(); * * setTimeout(function() { - * // since this fn executes async in a future turn of the event loop, we need to wrap - * // our code into an $apply call so that the model changes are properly observed. - * scope.$apply(function() { - * deferred.notify('About to greet ' + name + '.'); + * deferred.notify('About to greet ' + name + '.'); * - * if (okToGreet(name)) { - * deferred.resolve('Hello, ' + name + '!'); - * } else { - * deferred.reject('Greeting ' + name + ' is not allowed.'); - * } - * }); + * if (okToGreet(name)) { + * deferred.resolve('Hello, ' + name + '!'); + * } else { + * deferred.reject('Greeting ' + name + ' is not allowed.'); + * } * }, 1000); * * return deferred.promise; @@ -11123,7 +12954,6 @@ function $ParseProvider() { * For more on this please see the [Q documentation](https://github.com/kriskowal/q) especially the * section on serial or parallel joining of promises. * - * * # The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. @@ -11164,21 +12994,17 @@ function $ParseProvider() { * * 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 can not be resolved or rejected from the notifyCallback + * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback * method. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * - * - `finally(callback)` – allows you to observe either the fulfillment or rejection of a promise, + * - `finally(callback, notifyCallback)` – allows you to observe either the fulfillment or rejection of a promise, * but to do so without modifying the final value. This is useful to release resources or do some * clean-up that needs to be done whether the promise was rejected or resolved. See the [full * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * - * Because `finally` is a reserved word in JavaScript and reserved keywords are not supported as - * property names by ES3, you'll need to invoke the method like `promise['finally'](callback)` to - * make your code IE8 and Android 2.x compatible. - * * # Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily @@ -11232,6 +13058,12 @@ function $ParseProvider() { * expect(resolvedValue).toEqual(123); * })); * ``` + * + * @param {function(function, function)} resolver Function which is responsible for resolving or + * rejecting the newly created promise. The first parameter is a function which resolves the + * promise, the second parameter is a function which rejects the promise. + * + * @returns {Promise} The newly created promise. */ function $QProvider() { @@ -11242,21 +13074,41 @@ function $QProvider() { }]; } +function $$QProvider() { + this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { + return qFactory(function(callback) { + $browser.defer(callback); + }, $exceptionHandler); + }]; +} /** * Constructs a promise manager. * - * @param {function(Function)} nextTick Function for executing functions in the next turn. + * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. * @returns {object} Promise manager. */ function qFactory(nextTick, exceptionHandler) { + var $qMinErr = minErr('$q', TypeError); + function callOnce(self, resolveFn, rejectFn) { + var called = false; + function wrap(fn) { + return function(value) { + if (called) return; + called = true; + fn.call(self, value); + }; + } + + return [wrap(resolveFn), wrap(rejectFn)]; + } /** * @ngdoc method - * @name $q#defer - * @function + * @name ng.$q#defer + * @kind function * * @description * Creates a `Deferred` object which represents a task which will finish in the future. @@ -11264,156 +13116,152 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Deferred} Returns a new instance of deferred. */ var defer = function() { - var pending = [], - value, deferred; - - deferred = { - - resolve: function(val) { - if (pending) { - var callbacks = pending; - pending = undefined; - value = ref(val); - - if (callbacks.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - value.then(callback[0], callback[1], callback[2]); - } - }); - } - } - }, - + return new Deferred(); + }; - reject: function(reason) { - deferred.resolve(createInternalRejectedPromise(reason)); - }, + function Promise() { + this.$$state = { status: 0 }; + } + Promise.prototype = { + then: function(onFulfilled, onRejected, progressBack) { + var result = new Deferred(); - notify: function(progress) { - if (pending) { - var callbacks = pending; - - if (pending.length) { - nextTick(function() { - var callback; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - callback = callbacks[i]; - callback[2](progress); - } - }); - } - } - }, + this.$$state.pending = this.$$state.pending || []; + this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); + if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); + return result.promise; + }, - promise: { - then: function(callback, errback, progressback) { - var result = defer(); + "catch": function(callback) { + return this.then(null, callback); + }, - var wrappedCallback = function(value) { - try { - result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + "finally": function(callback, progressBack) { + return this.then(function(value) { + return handleCallback(value, true, callback); + }, function(error) { + return handleCallback(error, false, callback); + }, progressBack); + } + }; - var wrappedErrback = function(reason) { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }; + //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native + function simpleBind(context, fn) { + return function(value) { + fn.call(context, value); + }; + } - var wrappedProgressback = function(progress) { - try { - result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); - } catch(e) { - exceptionHandler(e); - } - }; + function processQueue(state) { + var fn, promise, pending; - if (pending) { - pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); - } else { - value.then(wrappedCallback, wrappedErrback, wrappedProgressback); - } + pending = state.pending; + state.processScheduled = false; + state.pending = undefined; + for (var i = 0, ii = pending.length; i < ii; ++i) { + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + promise.resolve(fn(state.value)); + } else if (state.status === 1) { + promise.resolve(state.value); + } else { + promise.reject(state.value); + } + } catch (e) { + promise.reject(e); + exceptionHandler(e); + } + } + } - return result.promise; - }, + function scheduleProcessQueue(state) { + if (state.processScheduled || !state.pending) return; + state.processScheduled = true; + nextTick(function() { processQueue(state); }); + } - "catch": function(callback) { - return this.then(null, callback); - }, + function Deferred() { + this.promise = new Promise(); + //Necessary to support unbound execution :/ + this.resolve = simpleBind(this, this.resolve); + this.reject = simpleBind(this, this.reject); + this.notify = simpleBind(this, this.notify); + } - "finally": function(callback) { + Deferred.prototype = { + resolve: function(val) { + if (this.promise.$$state.status) return; + if (val === this.promise) { + this.$$reject($qMinErr( + 'qcycle', + "Expected promise to be resolved with value other than itself '{0}'", + val)); + } + else { + this.$$resolve(val); + } - function makePromise(value, resolved) { - var result = defer(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - } + }, - function handleCallback(value, isResolved) { - var callbackOutput = null; - try { - callbackOutput = (callback ||defaultCallback)(); - } catch(e) { - return makePromise(e, false); - } - if (callbackOutput && isFunction(callbackOutput.then)) { - return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); - } else { - return makePromise(value, isResolved); - } - } + $$resolve: function(val) { + var then, fns; - return this.then(function(value) { - return handleCallback(value, true); - }, function(error) { - return handleCallback(error, false); - }); + fns = callOnce(this, this.$$resolve, this.$$reject); + try { + if ((isObject(val) || isFunction(val))) then = val && val.then; + if (isFunction(then)) { + this.promise.$$state.status = -1; + then.call(val, fns[0], fns[1], this.notify); + } else { + this.promise.$$state.value = val; + this.promise.$$state.status = 1; + scheduleProcessQueue(this.promise.$$state); } + } catch (e) { + fns[1](e); + exceptionHandler(e); } - }; + }, - return deferred; - }; + reject: function(reason) { + if (this.promise.$$state.status) return; + this.$$reject(reason); + }, + $$reject: function(reason) { + this.promise.$$state.value = reason; + this.promise.$$state.status = 2; + scheduleProcessQueue(this.promise.$$state); + }, - var ref = function(value) { - if (value && isFunction(value.then)) return value; - return { - then: function(callback) { - var result = defer(); + notify: function(progress) { + var callbacks = this.promise.$$state.pending; + + if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { nextTick(function() { - result.resolve(callback(value)); + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + result.notify(isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); + } + } }); - return result.promise; } - }; + } }; - /** * @ngdoc method * @name $q#reject - * @function + * @kind function * * @description * Creates a promise that is resolved as rejected with the specified `reason`. This api should be @@ -11447,33 +13295,43 @@ function qFactory(nextTick, exceptionHandler) { * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ var reject = function(reason) { - var result = defer(); + var result = new Deferred(); result.reject(reason); return result.promise; }; - var createInternalRejectedPromise = function(reason) { - return { - then: function(callback, errback) { - var result = defer(); - nextTick(function() { - try { - result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); - } catch(e) { - result.reject(e); - exceptionHandler(e); - } - }); - return result.promise; - } - }; + var makePromise = function makePromise(value, resolved) { + var result = new Deferred(); + if (resolved) { + result.resolve(value); + } else { + result.reject(value); + } + return result.promise; }; + var handleCallback = function handleCallback(value, isResolved, callback) { + var callbackOutput = null; + try { + if (isFunction(callback)) callbackOutput = callback(); + } catch (e) { + return makePromise(e, false); + } + if (isPromiseLike(callbackOutput)) { + return callbackOutput.then(function() { + return makePromise(value, isResolved); + }, function(error) { + return makePromise(error, false); + }); + } else { + return makePromise(value, isResolved); + } + }; /** * @ngdoc method * @name $q#when - * @function + * @kind function * * @description * Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. @@ -11483,69 +13341,18 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} value Value or a promise * @returns {Promise} Returns a promise of the passed value or promise */ - var when = function(value, callback, errback, progressback) { - var result = defer(), - done; - var wrappedCallback = function(value) { - try { - return (isFunction(callback) ? callback : defaultCallback)(value); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - - var wrappedErrback = function(reason) { - try { - return (isFunction(errback) ? errback : defaultErrback)(reason); - } catch (e) { - exceptionHandler(e); - return reject(e); - } - }; - var wrappedProgressback = function(progress) { - try { - return (isFunction(progressback) ? progressback : defaultCallback)(progress); - } catch (e) { - exceptionHandler(e); - } - }; - - nextTick(function() { - ref(value).then(function(value) { - if (done) return; - done = true; - result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); - }, function(reason) { - if (done) return; - done = true; - result.resolve(wrappedErrback(reason)); - }, function(progress) { - if (done) return; - result.notify(wrappedProgressback(progress)); - }); - }); - - return result.promise; + var when = function(value, callback, errback, progressBack) { + var result = new Deferred(); + result.resolve(value); + return result.promise.then(callback, errback, progressBack); }; - - function defaultCallback(value) { - return value; - } - - - function defaultErrback(reason) { - return reject(reason); - } - - /** * @ngdoc method * @name $q#all - * @function + * @kind function * * @description * Combines multiple promises into a single promise that is resolved when all of the input @@ -11557,14 +13364,15 @@ function qFactory(nextTick, exceptionHandler) { * If any of the promises is resolved with a rejection, this resulting promise will be rejected * with the same rejection value. */ + function all(promises) { - var deferred = defer(), + var deferred = new Deferred(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; - ref(promise).then(function(value) { + when(promise).then(function(value) { if (results.hasOwnProperty(key)) return; results[key] = value; if (!(--counter)) deferred.resolve(results); @@ -11581,23 +13389,46 @@ function qFactory(nextTick, exceptionHandler) { return deferred.promise; } - return { - defer: defer, - reject: reject, - when: when, - all: all + var $Q = function Q(resolver) { + if (!isFunction(resolver)) { + throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); + } + + if (!(this instanceof Q)) { + // More useful when $Q is the Promise itself. + return new Q(resolver); + } + + var deferred = new Deferred(); + + function resolveFn(value) { + deferred.resolve(value); + } + + function rejectFn(reason) { + deferred.reject(reason); + } + + resolver(resolveFn, rejectFn); + + return deferred.promise; }; + + $Q.defer = defer; + $Q.reject = reject; + $Q.when = when; + $Q.all = all; + + return $Q; } -function $$RAFProvider(){ //rAF +function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || - $window.webkitRequestAnimationFrame || - $window.mozRequestAnimationFrame; + $window.webkitRequestAnimationFrame; var cancelAnimationFrame = $window.cancelAnimationFrame || $window.webkitCancelAnimationFrame || - $window.mozCancelAnimationFrame || $window.webkitCancelRequestAnimationFrame; var rafSupported = !!requestAnimationFrame; @@ -11636,7 +13467,7 @@ function $$RAFProvider(){ //rAF * * 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 - * items to the array at the beginning (shift) instead of at the end (push) + * 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 @@ -11688,10 +13519,11 @@ function $$RAFProvider(){ //rAF * They also provide an event emission/broadcast and subscription facility. See the * {@link guide/scope developer guide on scopes}. */ -function $RootScopeProvider(){ +function $RootScopeProvider() { var TTL = 10; var $rootScopeMinErr = minErr('$rootScope'); var lastDirtyWatch = null; + var applyAsyncId = null; this.digestTtl = function(value) { if (arguments.length) { @@ -11701,7 +13533,7 @@ function $RootScopeProvider(){ }; this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function( $injector, $exceptionHandler, $parse, $browser) { + function($injector, $exceptionHandler, $parse, $browser) { /** * @ngdoc type @@ -11725,7 +13557,6 @@ function $RootScopeProvider(){ var child = parent.$new(); parent.salutation = "Hello"; - child.name = "World"; expect(child.salutation).toEqual('Hello'); child.salutation = "Welcome"; @@ -11733,6 +13564,10 @@ function $RootScopeProvider(){ expect(parent.salutation).toEqual('Hello'); * ``` * + * When interacting with `Scope` in tests, additional helper methods are available on the + * instances of `Scope` type. See {@link ngMock.$rootScope.Scope ngMock Scope} for additional + * details. + * * * @param {Object.<string, function()>=} providers Map of service factory which need to be * provided for the current scope. Defaults to {@link ng}. @@ -11748,36 +13583,49 @@ function $RootScopeProvider(){ this.$$phase = this.$parent = this.$$watchers = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = null; - this['this'] = this.$root = this; + this.$root = this; this.$$destroyed = false; - this.$$asyncQueue = []; - this.$$postDigestQueue = []; this.$$listeners = {}; this.$$listenerCount = {}; - this.$$isolateBindings = {}; + this.$$isolateBindings = null; } /** * @ngdoc property * @name $rootScope.Scope#$id - * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for - * debugging. + * + * @description + * Unique scope ID (monotonically increasing) useful for debugging. */ + /** + * @ngdoc property + * @name $rootScope.Scope#$parent + * + * @description + * Reference to the parent scope. + */ + + /** + * @ngdoc property + * @name $rootScope.Scope#$root + * + * @description + * Reference to the root scope. + */ Scope.prototype = { constructor: Scope, /** * @ngdoc method * @name $rootScope.Scope#$new - * @function + * @kind function * * @description * Creates a new child {@link ng.$rootScope.Scope scope}. * - * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and - * {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the - * scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. + * The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} event. + * The scope can be removed from the scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}. * * {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is * desired for the scope and its child scopes to be permanently detached from the parent and @@ -11788,46 +13636,66 @@ function $RootScopeProvider(){ * When creating widgets, it is useful for the widget to not accidentally read parent * state. * + * @param {Scope} [parent=this] The {@link ng.$rootScope.Scope `Scope`} that will be the `$parent` + * of the newly created scope. Defaults to `this` scope if not provided. + * This is used when creating a transclude scope to correctly place it + * in the scope hierarchy while maintaining the correct prototypical + * inheritance. + * * @returns {Object} The newly created child scope. * */ - $new: function(isolate) { - var ChildScope, - child; + $new: function(isolate, parent) { + var child; + + parent = parent || this; if (isolate) { child = new Scope(); child.$root = this.$root; - // ensure that there is just one async queue per $rootScope and its children - child.$$asyncQueue = this.$$asyncQueue; - child.$$postDigestQueue = this.$$postDigestQueue; } else { - ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges - // the name it does not become random set of chars. This will then show up as class - // name in the web inspector. - ChildScope.prototype = this; - child = new ChildScope(); - child.$id = nextUid(); + // Only create a child scope class if somebody asks for one, + // but cache it to allow the VM to optimize lookups. + if (!this.$$ChildScope) { + this.$$ChildScope = function ChildScope() { + this.$$watchers = this.$$nextSibling = + this.$$childHead = this.$$childTail = null; + this.$$listeners = {}; + this.$$listenerCount = {}; + this.$id = nextUid(); + this.$$ChildScope = null; + }; + this.$$ChildScope.prototype = this; + } + child = new this.$$ChildScope(); } - child['this'] = child; - child.$$listeners = {}; - child.$$listenerCount = {}; - child.$parent = this; - child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null; - child.$$prevSibling = this.$$childTail; - if (this.$$childHead) { - this.$$childTail.$$nextSibling = child; - this.$$childTail = child; + child.$parent = parent; + child.$$prevSibling = parent.$$childTail; + if (parent.$$childHead) { + parent.$$childTail.$$nextSibling = child; + parent.$$childTail = child; } else { - this.$$childHead = this.$$childTail = child; + parent.$$childHead = parent.$$childTail = child; } + + // When the new scope is not isolated or we inherit from `this`, and + // the parent scope is destroyed, the property `$$destroyed` is inherited + // prototypically. In all other cases, this property needs to be set + // when the parent scope is destroyed. + // The listener needs to be added after the parent is set + if (isolate || parent != this) child.$on('$destroy', destroyChild); + return child; + + function destroyChild() { + child.$$destroyed = true; + } }, /** * @ngdoc method * @name $rootScope.Scope#$watch - * @function + * @kind function * * @description * Registers a `listener` callback to be executed whenever the `watchExpression` changes. @@ -11839,10 +13707,14 @@ function $RootScopeProvider(){ * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.) * - 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). The inequality is determined according to - * {@link angular.equals} function. To save the value of the object for later comparison, - * the {@link angular.copy} function is used. It also means that watching complex options - * will have adverse memory and performance implications. + * see below). Inequality is determined according to reference inequality, + * [strict comparison](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators) + * via the `!==` Javascript operator, unless `objectEquality == true` + * (see next point) + * - When `objectEquality == true`, inequality of the `watchExpression` is determined + * according to the {@link angular.equals} function. To save the value of the object for + * later comparison, the {@link angular.copy} function is used. This therefore means that + * watching complex objects will have adverse memory and performance implications. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -11860,7 +13732,6 @@ function $RootScopeProvider(){ * can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the * listener was called due to initialization. * - * The example below contains an illustration of using a function as your $watch listener * * * # Example @@ -11877,23 +13748,27 @@ function $RootScopeProvider(){ expect(scope.counter).toEqual(0); scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - scope.name = 'adam'; scope.$digest(); + // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); - // Using a listener function + + // Using a function as a watchExpression var food; scope.foodCounter = 0; expect(scope.foodCounter).toEqual(0); scope.$watch( - // This is the listener function + // This function returns the value being watched. It is called for each turn of the $digest loop function() { return food; }, - // This is the change handler + // This is the change listener, called when the value returned from the above function changes function(newValue, oldValue) { if ( newValue !== oldValue ) { // Only increment the counter if the value changed @@ -11923,20 +13798,23 @@ function $RootScopeProvider(){ * * - `string`: Evaluated as {@link guide/expression expression} * - `function(scope)`: called with current `scope` as a parameter. - * @param {(function()|string)=} listener Callback called whenever the return value of - * the `watchExpression` changes. - * - * - `string`: Evaluated as {@link guide/expression expression} - * - `function(newValue, oldValue, scope)`: called with current and previous values as - * parameters. + * @param {function(newVal, oldVal, scope)} listener Callback called whenever the value + * of `watchExpression` changes. * + * - `newVal` contains the current value of the `watchExpression` + * - `oldVal` contains the previous value of the `watchExpression` + * - `scope` refers to the current scope * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of * comparing for reference equality. * @returns {function()} Returns a deregistration function for this listener. */ $watch: function(watchExp, listener, objectEquality) { + var get = $parse(watchExp); + + if (get.$$watchDelegate) { + return get.$$watchDelegate(this, listener, objectEquality, get); + } var scope = this, - get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, @@ -11948,18 +13826,8 @@ function $RootScopeProvider(){ lastDirtyWatch = null; - // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { - var listenFn = compileToFn(listener || noop, 'listener'); - watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; - } - - if (typeof watchExp == 'string' && get.constant) { - var originalFn = watcher.fn; - watcher.fn = function(newVal, oldVal, scope) { - originalFn.call(this, newVal, oldVal, scope); - arrayRemove(array, watcher); - }; + watcher.fn = noop; } if (!array) { @@ -11969,17 +13837,100 @@ function $RootScopeProvider(){ // the while loop reads in reverse order. array.unshift(watcher); - return function() { + return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$watchGroup + * @kind function + * + * @description + * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. + * If any one expression in the collection changes the `listener` is executed. + * + * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every + * call to $digest() to see if any items changes. + * - The `listener` is called whenever any expression in the `watchExpressions` array changes. + * + * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually + * watched using {@link ng.$rootScope.Scope#$watch $watch()} + * + * @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any + * expression in `watchExpressions` changes + * The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching + * those of `watchExpression` + * The `scope` refers to the current scope. + * @returns {function()} Returns a de-registration function for all listeners. + */ + $watchGroup: function(watchExpressions, listener) { + var oldValues = new Array(watchExpressions.length); + var newValues = new Array(watchExpressions.length); + var deregisterFns = []; + var self = this; + var changeReactionScheduled = false; + var firstRun = true; + + if (!watchExpressions.length) { + // No expressions means we call the listener ASAP + var shouldCall = true; + self.$evalAsync(function() { + if (shouldCall) listener(newValues, newValues, self); + }); + return function deregisterWatchGroup() { + shouldCall = false; + }; + } + + if (watchExpressions.length === 1) { + // Special case size of one + return this.$watch(watchExpressions[0], function watchGroupAction(value, oldValue, scope) { + newValues[0] = value; + oldValues[0] = oldValue; + listener(newValues, (value === oldValue) ? newValues : oldValues, scope); + }); + } + + forEach(watchExpressions, function(expr, i) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + newValues[i] = value; + oldValues[i] = oldValue; + if (!changeReactionScheduled) { + changeReactionScheduled = true; + self.$evalAsync(watchGroupAction); + } + }); + deregisterFns.push(unwatchFn); + }); + + function watchGroupAction() { + changeReactionScheduled = false; + + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } + + return function deregisterWatchGroup() { + while (deregisterFns.length) { + deregisterFns.shift()(); + } + }; + }, + /** * @ngdoc method * @name $rootScope.Scope#$watchCollection - * @function + * @kind function * * @description * Shallow watches the properties of an object and fires whenever any of the properties change @@ -12032,6 +13983,8 @@ function $RootScopeProvider(){ * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { + $watchCollectionInterceptor.$stateful = true; + var self = this; // the current value, updated on each dirty-check run var newValue; @@ -12043,15 +13996,18 @@ function $RootScopeProvider(){ // only track veryOldValue if the listener is asking for it var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; - var objGetter = $parse(obj); + var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; - function $watchCollectionWatch() { - newValue = objGetter(self); - var newLength, key; + function $watchCollectionInterceptor(_value) { + newValue = _value; + var newLength, key, bothNaN, newItem, oldItem; + + // If the new value is undefined, then return undefined as the watch may be a one-time watch + if (isUndefined(newValue)) return; if (!isObject(newValue)) { // if primitive if (oldValue !== newValue) { @@ -12075,11 +14031,13 @@ function $RootScopeProvider(){ } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { - var bothNaN = (oldValue[i] !== oldValue[i]) && - (newValue[i] !== newValue[i]); - if (!bothNaN && (oldValue[i] !== newValue[i])) { + oldItem = oldValue[i]; + newItem = newValue[i]; + + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[i] = newValue[i]; + oldValue[i] = newItem; } } } else { @@ -12094,14 +14052,18 @@ function $RootScopeProvider(){ for (key in newValue) { if (newValue.hasOwnProperty(key)) { newLength++; - if (oldValue.hasOwnProperty(key)) { - if (oldValue[key] !== newValue[key]) { + newItem = newValue[key]; + oldItem = oldValue[key]; + + if (key in oldValue) { + bothNaN = (oldItem !== oldItem) && (newItem !== newItem); + if (!bothNaN && (oldItem !== newItem)) { changeDetected++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; } } else { oldLength++; - oldValue[key] = newValue[key]; + oldValue[key] = newItem; changeDetected++; } } @@ -12109,8 +14071,8 @@ function $RootScopeProvider(){ if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; - for(key in oldValue) { - if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) { + for (key in oldValue) { + if (!newValue.hasOwnProperty(key)) { oldLength--; delete oldValue[key]; } @@ -12149,13 +14111,13 @@ function $RootScopeProvider(){ } } - return this.$watch($watchCollectionWatch, $watchCollectionAction); + return this.$watch(changeDetector, $watchCollectionAction); }, /** * @ngdoc method * @name $rootScope.Scope#$digest - * @function + * @kind function * * @description * Processes all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and @@ -12169,7 +14131,7 @@ function $RootScopeProvider(){ * {@link ng.directive:ngController controllers} or in * {@link ng.$compileProvider#directive directives}. * Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within - * a {@link ng.$compileProvider#directive directives}), which will force a `$digest()`. + * a {@link ng.$compileProvider#directive directive}), which will force a `$digest()`. * * If you want to be notified whenever `$digest()` is called, * you can register a `watchExpression` function with @@ -12190,20 +14152,22 @@ function $RootScopeProvider(){ expect(scope.counter).toEqual(0); scope.$digest(); - // no variable change - expect(scope.counter).toEqual(0); + // the listener is always called during the first $digest loop after it was registered + expect(scope.counter).toEqual(1); - scope.name = 'adam'; scope.$digest(); + // but now it will not be called unless the value changes expect(scope.counter).toEqual(1); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(2); * ``` * */ $digest: function() { var watch, value, last, watchers, - asyncQueue = this.$$asyncQueue, - postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, @@ -12211,6 +14175,15 @@ function $RootScopeProvider(){ logIdx, logMsg, asyncTask; beginPhase('$digest'); + // Check for changes to browser url that happened in sync before the call to $digest + $browser.$$checkUrlChange(); + + if (this === $rootScope && applyAsyncId !== null) { + // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then + // cancel the scheduled $apply and flush the queue of expressions to be evaluated. + $browser.defer.cancel(applyAsyncId); + flushApplyAsync(); + } lastDirtyWatch = null; @@ -12218,12 +14191,11 @@ function $RootScopeProvider(){ dirty = false; current = target; - while(asyncQueue.length) { + while (asyncQueue.length) { try { asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression); + asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); } catch (e) { - clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; @@ -12243,20 +14215,20 @@ function $RootScopeProvider(){ if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value == 'number' && typeof last == 'number' + : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; - watch.last = watch.eq ? copy(value) : value; + watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; - logMsg = (isFunction(watch.exp)) - ? 'fn: ' + (watch.exp.name || watch.exp.toString()) - : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); - watchLog[logIdx].push(logMsg); + watchLog[logIdx].push({ + msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, + newVal: value, + oldVal: last + }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers @@ -12266,7 +14238,6 @@ function $RootScopeProvider(){ } } } catch (e) { - clearPhase(); $exceptionHandler(e); } } @@ -12277,7 +14248,7 @@ function $RootScopeProvider(){ // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { + while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } @@ -12285,19 +14256,19 @@ function $RootScopeProvider(){ // `break traverseScopesLoop;` takes us to here - if((dirty || asyncQueue.length) && !(ttl--)) { + if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); + TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); - while(postDigestQueue.length) { + while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { @@ -12322,7 +14293,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$destroy - * @function + * @kind function * * @description * Removes the current scope (and all of its children) from the parent scope. Removal implies @@ -12350,7 +14321,9 @@ function $RootScopeProvider(){ this.$$destroyed = true; if (this === $rootScope) return; - forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); + 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) @@ -12359,6 +14332,10 @@ function $RootScopeProvider(){ if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + // Disable listeners, watchers and apply/digest methods + this.$destroy = this.$digest = this.$apply = this.$evalAsync = this.$applyAsync = noop; + 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. @@ -12369,21 +14346,13 @@ function $RootScopeProvider(){ // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = - this.$$childTail = this.$root = null; - - // don't reset these to null in case some async task tries to register a listener/watch/task - this.$$listeners = {}; - this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; - - // prevent NPEs since these methods have references to properties we nulled out - this.$destroy = this.$digest = this.$apply = noop; - this.$on = this.$watch = function() { return noop; }; + this.$$childTail = this.$root = this.$$watchers = null; }, /** * @ngdoc method * @name $rootScope.Scope#$eval - * @function + * @kind function * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in @@ -12415,7 +14384,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$evalAsync - * @function + * @kind function * * @description * Executes the expression on the current scope at a later point in time. @@ -12440,29 +14409,30 @@ function $RootScopeProvider(){ * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. * + * @param {(object)=} locals Local variables object, useful for overriding values in scope. */ - $evalAsync: function(expr) { + $evalAsync: function(expr, locals) { // if we are outside of an $digest loop and this is the first time we are scheduling async // task also schedule async auto-flush - if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { + if (!$rootScope.$$phase && !asyncQueue.length) { $browser.defer(function() { - if ($rootScope.$$asyncQueue.length) { + if (asyncQueue.length) { $rootScope.$digest(); } }); } - this.$$asyncQueue.push({scope: this, expression: expr}); + asyncQueue.push({scope: this, expression: expr, locals: locals}); }, - $$postDigest : function(fn) { - this.$$postDigestQueue.push(fn); + $$postDigest: function(fn) { + postDigestQueue.push(fn); }, /** * @ngdoc method * @name $rootScope.Scope#$apply - * @function + * @kind function * * @description * `$apply()` is used to execute an expression in angular from outside of the angular @@ -12523,8 +14493,35 @@ function $RootScopeProvider(){ /** * @ngdoc method + * @name $rootScope.Scope#$applyAsync + * @kind function + * + * @description + * Schedule the invokation of $apply to occur at a later time. The actual time difference + * varies across browsers, but is typically around ~10 milliseconds. + * + * This can be used to queue up multiple expressions which need to be evaluated in the same + * digest. + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/expression expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + */ + $applyAsync: function(expr) { + var scope = this; + expr && applyAsyncQueue.push($applyAsyncExpression); + scheduleApplyAsync(); + + function $applyAsyncExpression() { + scope.$eval(expr); + } + }, + + /** + * @ngdoc method * @name $rootScope.Scope#$on - * @function + * @kind function * * @description * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for @@ -12535,7 +14532,8 @@ function $RootScopeProvider(){ * * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or * `$broadcast`-ed. - * - `currentScope` - `{Scope}`: the current scope which is handling the event. + * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the + * event propagates through the scope hierarchy, this property is set to null. * - `name` - `{string}`: name of the event. * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel * further event propagation (available only for events that were `$emit`-ed). @@ -12564,8 +14562,11 @@ function $RootScopeProvider(){ var self = this; return function() { - namedListeners[indexOf(namedListeners, listener)] = null; - decrementListenerCount(self, 1, name); + var indexOfListener = namedListeners.indexOf(listener); + if (indexOfListener !== -1) { + namedListeners[indexOfListener] = null; + decrementListenerCount(self, 1, name); + } }; }, @@ -12573,7 +14574,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$emit - * @function + * @kind function * * @description * Dispatches an event `name` upwards through the scope hierarchy notifying the @@ -12612,7 +14613,7 @@ function $RootScopeProvider(){ do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; - for (i=0, length=namedListeners.length; i<length; i++) { + for (i = 0, length = namedListeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!namedListeners[i]) { @@ -12629,11 +14630,16 @@ function $RootScopeProvider(){ } } //if any listener on the current scope stops propagation, prevent bubbling - if (stopPropagation) return event; + if (stopPropagation) { + event.currentScope = null; + return event; + } //traverse upwards scope = scope.$parent; } while (scope); + event.currentScope = null; + return event; }, @@ -12641,7 +14647,7 @@ function $RootScopeProvider(){ /** * @ngdoc method * @name $rootScope.Scope#$broadcast - * @function + * @kind function * * @description * Dispatches an event `name` downwards to all child scopes (and their children) notifying the @@ -12670,15 +14676,18 @@ function $RootScopeProvider(){ event.defaultPrevented = true; }, defaultPrevented: false - }, - listenerArgs = concat([event], arguments, 1), + }; + + if (!target.$$listenerCount[name]) return event; + + var listenerArgs = concat([event], arguments, 1), listeners, i, length; //down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; - for (i=0, length = listeners.length; i<length; i++) { + for (i = 0, length = listeners.length; i < length; i++) { // if listeners were deregistered, defragment the array if (!listeners[i]) { listeners.splice(i, 1); @@ -12689,7 +14698,7 @@ function $RootScopeProvider(){ try { listeners[i].apply(null, listenerArgs); - } catch(e) { + } catch (e) { $exceptionHandler(e); } } @@ -12700,18 +14709,24 @@ function $RootScopeProvider(){ // (though it differs due to having the extra check for $$listenerCount) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { - while(current !== target && !(next = current.$$nextSibling)) { + while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } + event.currentScope = null; return event; } }; var $rootScope = new Scope(); + //The internal queues. Expose them on the $rootScope for debugging/testing purposes. + var asyncQueue = $rootScope.$$asyncQueue = []; + var postDigestQueue = $rootScope.$$postDigestQueue = []; + var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + return $rootScope; @@ -12727,11 +14742,6 @@ function $RootScopeProvider(){ $rootScope.$$phase = null; } - function compileToFn(exp, name) { - var fn = $parse(exp); - assertArgFn(fn, name); - return fn; - } function decrementListenerCount(current, count, name) { do { @@ -12748,6 +14758,25 @@ function $RootScopeProvider(){ * because it's unique we can easily tell it apart from other values */ function initWatchVal() {} + + function flushApplyAsync() { + while (applyAsyncQueue.length) { + try { + applyAsyncQueue.shift()(); + } catch (e) { + $exceptionHandler(e); + } + } + applyAsyncId = null; + } + + function scheduleApplyAsync() { + if (applyAsyncId === null) { + applyAsyncId = $browser.defer(function() { + $rootScope.$apply(flushApplyAsync); + }); + } + } }]; } @@ -12757,7 +14786,7 @@ function $RootScopeProvider(){ */ function $$SanitizeUriProvider() { var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description @@ -12812,12 +14841,9 @@ function $$SanitizeUriProvider() { return function sanitizeUri(uri, isImage) { var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; var normalizedVal; - // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. - if (!msie || msie >= 8 ) { - normalizedVal = urlResolve(uri).href; - if (normalizedVal !== '' && !normalizedVal.match(regex)) { - return 'unsafe:'+normalizedVal; - } + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:' + normalizedVal; } return uri; }; @@ -12838,15 +14864,6 @@ var SCE_CONTEXTS = { // Helper functions follow. -// Copied from: -// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962 -// Prereq: s is a string. -function escapeForRegexp(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). - replace(/\x08/g, '\\x08'); -} - - function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -12889,7 +14906,7 @@ function adjustMatchers(matchers) { /** * @ngdoc service * @name $sceDelegate - * @function + * @kind function * * @description * @@ -12936,19 +14953,21 @@ function adjustMatchers(matchers) { * * Here is what a secure configuration for this scenario might look like: * - * <pre class="prettyprint"> - * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ - * // Allow same origin resource loads. - * 'self', - * // Allow loading from our assets domain. Notice the difference between * and **. - * 'http://srv*.assets.example.com/**']); - * - * // The blacklist overrides the whitelist so the open redirect here is blocked. - * $sceDelegateProvider.resourceUrlBlacklist([ - * 'http://myapp.example.com/clickThru**']); - * }); - * </pre> + * ``` + * angular.module('myApp', []).config(function($sceDelegateProvider) { + * $sceDelegateProvider.resourceUrlWhitelist([ + * // Allow same origin resource loads. + * 'self', + * // Allow loading from our assets domain. Notice the difference between * and **. + * 'http://srv*.assets.example.com/**' + * ]); + * + * // The blacklist overrides the whitelist so the open redirect here is blocked. + * $sceDelegateProvider.resourceUrlBlacklist([ + * 'http://myapp.example.com/clickThru**' + * ]); + * }); + * ``` */ function $SceDelegateProvider() { @@ -12961,7 +14980,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlWhitelist - * @function + * @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 @@ -12980,7 +14999,7 @@ function $SceDelegateProvider() { * @description * Sets/Gets the whitelist of trusted resource URLs. */ - this.resourceUrlWhitelist = function (value) { + this.resourceUrlWhitelist = function(value) { if (arguments.length) { resourceUrlWhitelist = adjustMatchers(value); } @@ -12990,7 +15009,7 @@ function $SceDelegateProvider() { /** * @ngdoc method * @name $sceDelegateProvider#resourceUrlBlacklist - * @function + * @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 @@ -13014,7 +15033,7 @@ function $SceDelegateProvider() { * Sets/Gets the blacklist of trusted resource URLs. */ - this.resourceUrlBlacklist = function (value) { + this.resourceUrlBlacklist = function(value) { if (arguments.length) { resourceUrlBlacklist = adjustMatchers(value); } @@ -13217,7 +15236,7 @@ function $SceDelegateProvider() { /** * @ngdoc service * @name $sce - * @function + * @kind function * * @description * @@ -13232,7 +15251,7 @@ function $SceDelegateProvider() { * * As of version 1.2, Angular ships with SCE enabled by default. * - * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows + * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow * one to execute arbitrary javascript by the use of the expression() syntax. Refer * <http://blogs.msdn.com/b/ie/archive/2008/10/16/ending-expressions.aspx> to learn more about them. * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>` @@ -13243,10 +15262,10 @@ function $SceDelegateProvider() { * * Here's an example of a binding in a privileged context: * - * <pre class="prettyprint"> - * <input ng-model="userHtml"> - * <div ng-bind-html="userHtml"> - * </pre> + * ``` + * <input ng-model="userHtml"> + * <div ng-bind-html="userHtml"></div> + * ``` * * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE * disabled, this application allows the user to render arbitrary HTML into the DIV. @@ -13279,22 +15298,22 @@ function $SceDelegateProvider() { * * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link - * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the + * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly * simplified): * - * <pre class="prettyprint"> - * var ngBindHtmlDirective = ['$sce', function($sce) { - * return function(scope, element, attr) { - * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { - * element.html(value || ''); - * }); - * }; - * }]; - * </pre> + * ``` + * var ngBindHtmlDirective = ['$sce', function($sce) { + * return function(scope, element, attr) { + * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) { + * element.html(value || ''); + * }); + * }; + * }]; + * ``` * * ## Impact on loading templates * @@ -13316,7 +15335,7 @@ function $SceDelegateProvider() { * won't work on all browsers. Also, loading templates from `file://` URL does not work on some * browsers. * - * ## This feels like too much overhead for the developer? + * ## This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * @@ -13343,7 +15362,7 @@ function $SceDelegateProvider() { * * | Context | Notes | * |---------------------|----------------| - * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. | + * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. | * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | @@ -13367,7 +15386,7 @@ function $SceDelegateProvider() { * - `**`: matches zero or more occurrences of *any* character. As such, it's not * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might - * not have been the intention.) It's usage at the very end of the path is ok. (e.g. + * not have been the intention.) Its usage at the very end of the path is ok. (e.g. * http://foo.example.com/templates/**). * - **RegExp** (*see caveat below*) * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax @@ -13398,66 +15417,65 @@ function $SceDelegateProvider() { * * ## Show me an example using SCE. * - * @example -<example module="mySceApp" deps="angular-sanitize.js"> -<file name="index.html"> - <div ng-controller="myAppController as myCtrl"> - <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> - <b>User comments</b><br> - By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when - $sanitize is available. If $sanitize isn't available, this results in an error instead of an - exploit. - <div class="well"> - <div ng-repeat="userComment in myCtrl.userComments"> - <b>{{userComment.name}}</b>: - <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> - <br> - </div> - </div> - </div> -</file> - -<file name="script.js"> - var mySceApp = angular.module('mySceApp', ['ngSanitize']); - - mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) { - var self = this; - $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - self.userComments = userComments; - }); - self.explicitlyTrustedHtml = $sce.trustAsHtml( - '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + - 'sanitization."">Hover over this text.</span>'); - }); -</file> - -<file name="test_data.json"> -[ - { "name": "Alice", - "htmlComment": - "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" - }, - { "name": "Bob", - "htmlComment": "<i>Yes!</i> Am I the only other one?" - } -] -</file> - -<file name="protractor.js" type="protractor"> - describe('SCE doc demo', function() { - it('should sanitize untrusted values', function() { - expect(element(by.css('.htmlComment')).getInnerHtml()) - .toBe('<span>Is <i>anyone</i> reading this?</span>'); - }); - - it('should NOT sanitize explicitly trusted values', function() { - expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( - '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + - 'sanitization."">Hover over this text.</span>'); - }); - }); -</file> -</example> + * <example module="mySceApp" deps="angular-sanitize.js"> + * <file name="index.html"> + * <div ng-controller="AppController as myCtrl"> + * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> + * <b>User comments</b><br> + * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when + * $sanitize is available. If $sanitize isn't available, this results in an error instead of an + * exploit. + * <div class="well"> + * <div ng-repeat="userComment in myCtrl.userComments"> + * <b>{{userComment.name}}</b>: + * <span ng-bind-html="userComment.htmlComment" class="htmlComment"></span> + * <br> + * </div> + * </div> + * </div> + * </file> + * + * <file name="script.js"> + * angular.module('mySceApp', ['ngSanitize']) + * .controller('AppController', ['$http', '$templateCache', '$sce', + * function($http, $templateCache, $sce) { + * var self = this; + * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { + * self.userComments = userComments; + * }); + * self.explicitlyTrustedHtml = $sce.trustAsHtml( + * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + + * 'sanitization."">Hover over this text.</span>'); + * }]); + * </file> + * + * <file name="test_data.json"> + * [ + * { "name": "Alice", + * "htmlComment": + * "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>" + * }, + * { "name": "Bob", + * "htmlComment": "<i>Yes!</i> Am I the only other one?" + * } + * ] + * </file> + * + * <file name="protractor.js" type="protractor"> + * describe('SCE doc demo', function() { + * it('should sanitize untrusted values', function() { + * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * .toBe('<span>Is <i>anyone</i> reading this?</span>'); + * }); + * + * it('should NOT sanitize explicitly trusted values', function() { + * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + + * 'sanitization."">Hover over this text.</span>'); + * }); + * }); + * </file> + * </example> * * * @@ -13471,13 +15489,13 @@ function $SceDelegateProvider() { * * That said, here's how you can completely disable SCE: * - * <pre class="prettyprint"> - * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { - * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. - * $sceProvider.enabled(false); - * }); - * </pre> + * ``` + * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { + * // Completely disable SCE. For demonstration purposes only! + * // Do not use in new projects. + * $sceProvider.enabled(false); + * }); + * ``` * */ /* jshint maxlen: 100 */ @@ -13488,7 +15506,7 @@ function $SceProvider() { /** * @ngdoc method * @name $sceProvider#enabled - * @function + * @kind function * * @param {boolean=} value If provided, then enables/disables SCE. * @return {boolean} true if SCE is enabled, false otherwise. @@ -13496,7 +15514,7 @@ function $SceProvider() { * @description * Enables/disables SCE and returns the current value. */ - this.enabled = function (value) { + this.enabled = function(value) { if (arguments.length) { enabled = !!value; } @@ -13550,23 +15568,23 @@ function $SceProvider() { * sce.js and sceSpecs.js would need to be aware of this detail. */ - this.$get = ['$parse', '$sniffer', '$sceDelegate', function( - $parse, $sniffer, $sceDelegate) { - // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows + this.$get = ['$parse', '$sceDelegate', function( + $parse, $sceDelegate) { + // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. - if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) { + if (enabled && msie < 8) { throw $sceMinErr('iequirks', - 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' + + '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.'); } - var sce = copy(SCE_CONTEXTS); + var sce = shallowCopy(SCE_CONTEXTS); /** * @ngdoc method * @name $sce#isEnabled - * @function + * @kind function * * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you * have to do it at module config time on {@link ng.$sceProvider $sceProvider}. @@ -13574,7 +15592,7 @@ function $SceProvider() { * @description * Returns a boolean indicating if SCE is enabled. */ - sce.isEnabled = function () { + sce.isEnabled = function() { return enabled; }; sce.trustAs = $sceDelegate.trustAs; @@ -13588,7 +15606,7 @@ function $SceProvider() { /** * @ngdoc method - * @name $sce#parse + * @name $sce#parseAs * * @description * Converts Angular {@link guide/expression expression} into a function. This is like {@link @@ -13610,9 +15628,9 @@ function $SceProvider() { if (parsed.literal && parsed.constant) { return parsed; } else { - return function sceParseAsTrusted(self, locals) { - return sce.getTrusted(type, parsed(self, locals)); - }; + return $parse(expr, function(value) { + return sce.getTrusted(type, value); + }); } }; @@ -13779,7 +15797,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsHtml(expression string)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13796,7 +15814,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsCss(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13813,7 +15831,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13830,7 +15848,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsResourceUrl(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13847,7 +15865,7 @@ function $SceProvider() { * * @description * Shorthand method. `$sce.parseAsJs(value)` → - * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`} + * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`} * * @param {string} expression String expression to compile. * @returns {function(context, locals)} a function which represents the compiled expression: @@ -13863,15 +15881,15 @@ function $SceProvider() { getTrusted = sce.getTrusted, trustAs = sce.trustAs; - forEach(SCE_CONTEXTS, function (enumValue, name) { + forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function (expr) { + sce[camelCase("parse_as_" + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function (value) { + sce[camelCase("get_trusted_" + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function (value) { + sce[camelCase("trust_as_" + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -13888,7 +15906,6 @@ function $SceProvider() { * @requires $document * * @property {boolean} history Does the browser support html5 history api ? - * @property {boolean} hashchange Does the browser support hashchange event ? * @property {boolean} transitions Does the browser support CSS transition events ? * @property {boolean} animations Does the browser support CSS animation events ? * @@ -13902,31 +15919,30 @@ function $SnifferProvider() { int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - documentMode = document.documentMode, vendorPrefix, - vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/, + vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, animations = false, match; if (bodyStyle) { - for(var prop in bodyStyle) { - if(match = vendorRegex.exec(prop)) { + for (var prop in bodyStyle) { + if (match = vendorRegex.exec(prop)) { vendorPrefix = match[0]; vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); break; } } - if(!vendorPrefix) { + if (!vendorPrefix) { vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; } transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - if (android && (!transitions||!animations)) { + if (android && (!transitions || !animations)) { transitions = isString(document.body.style.webkitTransition); animations = isString(document.body.style.webkitAnimation); } @@ -13945,14 +15961,13 @@ function $SnifferProvider() { // jshint -W018 history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), // jshint +W018 - hashchange: 'onhashchange' in $window && - // IE8 compatible mode lies - (!documentMode || documentMode > 7), hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. - if (event == 'input' && msie == 9) return false; + // IE10+ implements 'input' event but it erroneously fires under various situations, + // e.g. when placeholder changes, or a form is focused. + if (event === 'input' && msie <= 11) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -13963,18 +15978,192 @@ function $SnifferProvider() { }, csp: csp(), vendorPrefix: vendorPrefix, - transitions : transitions, - animations : animations, - android: android, - msie : msie, - msieDocumentMode: documentMode + transitions: transitions, + animations: animations, + android: android + }; + }]; +} + +var $compileMinErr = minErr('$compile'); + +/** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service downloads the provided template using `$http` and, upon success, + * stores the contents inside of `$templateCache`. If the HTTP request fails or the response data + * of the HTTP request is empty then a `$compile` error will be thrown (the exception can be thwarted + * by setting the 2nd parameter of the function to true). + * + * @param {string} 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. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ +function $TemplateRequestProvider() { + this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) { + function handleRequestFn(tpl, ignoreRequestError) { + var self = handleRequestFn; + self.totalPendingRequests++; + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + var httpOptions = { + cache: $templateCache, + transformResponse: transformResponse + }; + + return $http.get(tpl, httpOptions) + .then(function(response) { + self.totalPendingRequests--; + return response.data; + }, handleError); + + function handleError(resp) { + self.totalPendingRequests--; + if (!ignoreRequestError) { + throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl); + } + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + }]; +} + +function $$TestabilityProvider() { + this.$get = ['$rootScope', '$browser', '$location', + function($rootScope, $browser, $location) { + + /** + * @name $testability + * + * @description + * The private $$testability service provides a collection of methods for use when debugging + * or by automated test and debugging tools. + */ + var testability = {}; + + /** + * @name $$testability#findBindings + * + * @description + * Returns an array of elements that are bound (via ng-bind or {{}}) + * to expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The binding expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. Filters and whitespace are ignored. + */ + testability.findBindings = function(element, expression, opt_exactMatch) { + var bindings = element.getElementsByClassName('ng-binding'); + var matches = []; + forEach(bindings, function(binding) { + var dataBinding = angular.element(binding).data('$binding'); + if (dataBinding) { + forEach(dataBinding, function(bindingName) { + if (opt_exactMatch) { + var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)'); + if (matcher.test(bindingName)) { + matches.push(binding); + } + } else { + if (bindingName.indexOf(expression) != -1) { + matches.push(binding); + } + } + }); + } + }); + return matches; + }; + + /** + * @name $$testability#findModels + * + * @description + * Returns an array of elements that are two-way found via ng-model to + * expressions matching the input. + * + * @param {Element} element The element root to search from. + * @param {string} expression The model expression to match. + * @param {boolean} opt_exactMatch If true, only returns exact matches + * for the expression. + */ + testability.findModels = function(element, expression, opt_exactMatch) { + var prefixes = ['ng-', 'data-ng-', 'ng\\:']; + for (var p = 0; p < prefixes.length; ++p) { + var attributeEquals = opt_exactMatch ? '=' : '*='; + var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]'; + var elements = element.querySelectorAll(selector); + if (elements.length) { + return elements; + } + } + }; + + /** + * @name $$testability#getLocation + * + * @description + * Shortcut for getting the location in a browser agnostic way. Returns + * the path, search, and hash. (e.g. /path?a=b#hash) + */ + testability.getLocation = function() { + return $location.url(); + }; + + /** + * @name $$testability#setLocation + * + * @description + * Shortcut for navigating to a location without doing a full page reload. + * + * @param {string} url The location url (path, search and hash, + * e.g. /path?a=b#hash) to go to. + */ + testability.setLocation = function(url) { + if (url !== $location.url()) { + $location.url(url); + $rootScope.$digest(); + } + }; + + /** + * @name $$testability#whenStable + * + * @description + * Calls the callback when $timeout and $http requests are completed. + * + * @param {function} callback + */ + testability.whenStable = function(callback) { + $browser.notifyWhenNoOutstandingRequests(callback); }; + + return testability; }]; } function $TimeoutProvider() { - this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler', - function($rootScope, $browser, $q, $exceptionHandler) { + this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', + function($rootScope, $browser, $q, $$q, $exceptionHandler) { var deferreds = {}; @@ -14004,15 +16193,15 @@ function $TimeoutProvider() { * */ function timeout(fn, delay, invokeApply) { - var deferred = $q.defer(), + var skipApply = (isDefined(invokeApply) && !invokeApply), + deferred = (skipApply ? $$q : $q).defer(), promise = deferred.promise, - skipApply = (isDefined(invokeApply) && !invokeApply), timeoutId; timeoutId = $browser.defer(function() { try { deferred.resolve(fn()); - } catch(e) { + } catch (e) { deferred.reject(e); $exceptionHandler(e); } @@ -14063,7 +16252,7 @@ function $TimeoutProvider() { // exactly the behavior needed here. There is little value is mocking these out for this // service. var urlParsingNode = document.createElement("a"); -var originUrl = urlResolve(window.location.href, true); +var originUrl = urlResolve(window.location.href); /** @@ -14101,7 +16290,7 @@ var originUrl = urlResolve(window.location.href, true); * https://github.com/angular/angular.js/pull/2902 * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * - * @function + * @kind function * @param {string} url The URL to be parsed. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. @@ -14118,7 +16307,7 @@ var originUrl = urlResolve(window.location.href, true); * | pathname | The pathname, beginning with "/" * */ -function urlResolve(url, base) { +function urlResolve(url) { var href = url; if (msie) { @@ -14174,17 +16363,18 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - <example> + <example module="windowExample"> <file name="index.html"> <script> - function Ctrl($scope, $window) { - $scope.greeting = 'Hello, World!'; - $scope.doGreeting = function(greeting) { + angular.module('windowExample', []) + .controller('ExampleController', ['$scope', '$window', function($scope, $window) { + $scope.greeting = 'Hello, World!'; + $scope.doGreeting = function(greeting) { $window.alert(greeting); - }; - } + }; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="text" ng-model="greeting" /> <button ng-click="doGreeting(greeting)">ALERT</button> </div> @@ -14198,10 +16388,21 @@ function urlIsSameOrigin(requestUrl) { </file> </example> */ -function $WindowProvider(){ +function $WindowProvider() { this.$get = valueFn(window); } +/* global currencyFilter: true, + dateFilter: true, + filterFilter: true, + jsonFilter: true, + limitToFilter: true, + lowercaseFilter: true, + numberFilter: true, + orderByFilter: true, + uppercaseFilter: true, + */ + /** * @ngdoc provider * @name $filterProvider @@ -14251,21 +16452,11 @@ function $WindowProvider(){ * For more information about how angular filters work, and how to create your own filters, see * {@link guide/filter Filters} in the Angular Developer Guide. */ -/** - * @ngdoc method - * @name $filterProvider#register - * @description - * Register filter factory function. - * - * @param {String} name Name of the filter. - * @param {Function} fn The filter factory function which is injectable. - */ - /** * @ngdoc service * @name $filter - * @function + * @kind function * @description * Filters are used for formatting data displayed to the user. * @@ -14275,21 +16466,38 @@ function $WindowProvider(){ * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function - */ + * @example + <example name="$filter" module="filterExample"> + <file name="index.html"> + <div ng-controller="MainCtrl"> + <h3>{{ originalText }}</h3> + <h3>{{ filteredText }}</h3> + </div> + </file> + + <file name="script.js"> + angular.module('filterExample', []) + .controller('MainCtrl', function($scope, $filter) { + $scope.originalText = 'hello'; + $scope.filteredText = $filter('uppercase')($scope.originalText); + }); + </file> + </example> + */ $FilterProvider.$inject = ['$provide']; function $FilterProvider($provide) { var suffix = 'Filter'; /** * @ngdoc method - * @name $controllerProvider#register + * @name $filterProvider#register * @param {string|Object} name Name of the filter function, or an object map of filters where * the keys are the filter names and the values are the filter factories. * @returns {Object} Registered filter instance, or if a map of filters was provided then a map * of the registered filter instances. */ function register(name, factory) { - if(isObject(name)) { + if (isObject(name)) { var filters = {}; forEach(name, function(filter, key) { filters[key] = register(key, filter); @@ -14335,7 +16543,7 @@ function $FilterProvider($provide) { /** * @ngdoc filter * @name filter - * @function + * @kind function * * @description * Selects a subset of items from `array` and returns it as a new array. @@ -14355,11 +16563,13 @@ function $FilterProvider($provide) { * which have property `name` containing "M" and property `phone` containing "1". A special * property name `$` can be used (as in `{$:"text"}`) to accept a match against any * property of the object. That's equivalent to the simple substring match with a `string` - * as described above. + * as described above. The predicate can be negated by prefixing the string with `!`. + * For Example `{name: "!M"}` predicate will return an array of items which have property `name` + * not containing "M". * - * - `function(value)`: 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)`: 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. * * @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 @@ -14367,15 +16577,15 @@ function $FilterProvider($provide) { * * Can be one of: * - * - `function(actual, expected)`: - * The function will be given the object value and the predicate value to compare and - * should return true if the item should be included in filtered result. + * - `function(actual, expected)`: + * The function will be given the object value and the predicate value to compare and + * should return true if the item should be included in filtered result. * - * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. - * this is essentially strict comparison of expected and actual. + * - `true`: A shorthand for `function(actual, expected) { return angular.equals(expected, actual)}`. + * this is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false|undefined`: A short hand for a function which will look for a substring match in case + * insensitive way. * * @example <example> @@ -14449,112 +16659,109 @@ function filterFilter() { return function(array, expression, comparator) { if (!isArray(array)) return array; - var comparatorType = typeof(comparator), - predicates = []; + var predicateFn; + var matchAgainstAnyProp; - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - - if (comparatorType !== 'function') { - if (comparatorType === 'boolean' && comparator) { - comparator = function(obj, text) { - return angular.equals(obj, text); - }; - } else { - comparator = function(obj, text) { - if (obj && text && typeof obj === 'object' && typeof text === 'object') { - for (var objKey in obj) { - if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) && - comparator(obj[objKey], text[objKey])) { - return true; - } - } - return false; - } - text = (''+text).toLowerCase(); - return (''+obj).toLowerCase().indexOf(text) > -1; - }; - } - } - - var search = function(obj, text){ - if (typeof text == 'string' && text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return comparator(obj, text); - case "object": - switch (typeof text) { - case "object": - return comparator(obj, text); - default: - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - break; - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; switch (typeof expression) { - case "boolean": - case "number": - case "string": - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 - case "object": - // jshint +W086 - for (var key in expression) { - (function(path) { - if (typeof expression[path] == 'undefined') return; - predicates.push(function(value) { - return search(path == '$' ? value : (value && value[path]), expression[path]); - }); - })(key); - } - break; case 'function': - predicates.push(expression); + predicateFn = expression; + break; + case 'boolean': + case 'number': + case 'string': + matchAgainstAnyProp = true; + //jshint -W086 + case 'object': + //jshint +W086 + predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); break; default: return array; } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); + + return array.filter(predicateFn); + }; +} + +// Helper functions for `filterFilter` +function createPredicateFn(expression, comparator, matchAgainstAnyProp) { + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isObject(actual) || isObject(expected)) { + // Prevent an object to be considered equal to a string like `'[object'` + return false; } - } - return filtered; + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + return deepCompare(item, expression, comparator, matchAgainstAnyProp); }; + + return predicateFn; +} + +function deepCompare(actual, expected, comparator, matchAgainstAnyProp) { + var actualType = typeof actual; + var expectedType = typeof expected; + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); + } else if (actualType === 'array') { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator)) { + return true; + } + } + return false; + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal)) { + continue; + } + + var keyIsDollar = key === '$'; + var actualVal = keyIsDollar ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, keyIsDollar)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + break; + case 'function': + return false; + default: + return comparator(actual, expected); + } } /** * @ngdoc filter * @name currency - * @function + * @kind function * * @description * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default @@ -14562,27 +16769,31 @@ function filterFilter() { * * @param {number} amount Input to filter. * @param {string=} symbol Currency symbol or identifier to be displayed. + * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale * @returns {string} Formatted number. * * * @example - <example> + <example module="currencyExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.amount = 1234.56; - } + angular.module('currencyExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.amount = 1234.56; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <input type="number" ng-model="amount"> <br> default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br> - custom currency identifier (USD$): <span>{{amount | currency:"USD$"}}</span> + 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> </div> </file> <file name="protractor.js" type="protractor"> it('should init with 1234.56', function() { expect(element(by.id('currency-default')).getText()).toBe('$1,234.56'); - expect(element(by.binding('amount | currency:"USD$"')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56'); + expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { if (browser.params.browser == 'safari') { @@ -14593,7 +16804,8 @@ function filterFilter() { 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.binding('amount | currency:"USD$"')).getText()).toBe('(USD$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> @@ -14601,17 +16813,27 @@ function filterFilter() { currencyFilter.$inject = ['$locale']; function currencyFilter($locale) { var formats = $locale.NUMBER_FORMATS; - return function(amount, currencySymbol){ - if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM; - return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2). - replace(/\u00A4/g, currencySymbol); + return function(amount, currencySymbol, fractionSize) { + if (isUndefined(currencySymbol)) { + currencySymbol = formats.CURRENCY_SYM; + } + + if (isUndefined(fractionSize)) { + fractionSize = formats.PATTERNS[1].maxFrac; + } + + // if null or undefined pass it through + return (amount == null) + ? amount + : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize). + replace(/\u00A4/g, currencySymbol); }; } /** * @ngdoc filter * @name number - * @function + * @kind function * * @description * Formats a number as text. @@ -14625,14 +16847,15 @@ function currencyFilter($locale) { * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. * * @example - <example> + <example module="numberFilterExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = 1234.56789; - } + angular.module('numberFilterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = 1234.56789; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter number: <input ng-model='val'><br> Default formatting: <span id='number-default'>{{val | number}}</span><br> No fractions: <span>{{val | number:0}}</span><br> @@ -14662,14 +16885,18 @@ numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; return function(number, fractionSize) { - return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, - fractionSize); + + // if null or undefined pass it through + return (number == null) + ? number + : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP, + fractionSize); }; } var DECIMAL_SEP = '.'; function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (number == null || !isFinite(number) || isObject(number)) return ''; + if (!isFinite(number) || isObject(number)) return ''; var isNegative = number < 0; number = Math.abs(number); @@ -14681,7 +16908,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (numStr.indexOf('e') !== -1) { var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); if (match && match[2] == '-' && match[3] > fractionSize + 1) { - numStr = '0'; + number = 0; } else { formatedText = numStr; hasExponent = true; @@ -14696,8 +16923,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); } - var pow = Math.pow(10, fractionSize); - number = Math.round(number * pow) / pow; + // 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); + var fraction = ('' + number).split(DECIMAL_SEP); var whole = fraction[0]; fraction = fraction[1] || ''; @@ -14709,7 +16939,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { if (whole.length >= (lgroup + group)) { pos = whole.length - lgroup; for (i = 0; i < pos; i++) { - if ((pos - i)%group === 0 && i !== 0) { + if ((pos - i) % group === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); @@ -14717,28 +16947,32 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { } for (i = pos; i < whole.length; i++) { - if ((whole.length - i)%lgroup === 0 && i !== 0) { + if ((whole.length - i) % lgroup === 0 && i !== 0) { formatedText += groupSep; } formatedText += whole.charAt(i); } // format fraction part. - while(fraction.length < fractionSize) { + while (fraction.length < fractionSize) { fraction += '0'; } if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); } else { - - if (fractionSize > 0 && number > -1 && number < 1) { + if (fractionSize > 0 && number < 1) { formatedText = number.toFixed(fractionSize); + number = parseFloat(formatedText); } } - parts.push(isNegative ? pattern.negPre : pattern.posPre); - parts.push(formatedText); - parts.push(isNegative ? pattern.negSuf : pattern.posSuf); + if (number === 0) { + isNegative = false; + } + + parts.push(isNegative ? pattern.negPre : pattern.posPre, + formatedText, + isNegative ? pattern.negSuf : pattern.posSuf); return parts.join(''); } @@ -14749,7 +16983,7 @@ function padNumber(num, digits, trim) { num = -num; } num = '' + num; - while(num.length < digits) num = '0' + num; + while (num.length < digits) num = '0' + num; if (trim) num = num.substr(num.length - digits); return neg + num; @@ -14762,7 +16996,7 @@ function dateGetter(name, size, offset, trim) { var value = date['get' + name](); if (offset > 0 || value > -offset) value += offset; - if (value === 0 && offset == -12 ) value = 12; + if (value === 0 && offset == -12) value = 12; return padNumber(value, size, trim); }; } @@ -14786,6 +17020,32 @@ function timeZoneGetter(date) { return paddedZone; } +function getFirstThursdayOfYear(year) { + // 0 = index of January + var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay(); + // 4 = index of Thursday (+1 to account for 1st = 5) + // 11 = index of *next* Thursday (+1 account for 1st = 12) + return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst); +} + +function getThursdayThisWeek(datetime) { + return new Date(datetime.getFullYear(), datetime.getMonth(), + // 4 = index of Thursday + datetime.getDate() + (4 - datetime.getDay())); +} + +function weekGetter(size) { + return function(date) { + var firstThurs = getFirstThursdayOfYear(date.getFullYear()), + thisThurs = getThursdayThisWeek(date); + + var diff = +thisThurs - +firstThurs, + result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week + + return padNumber(result, size); + }; +} + function ampmGetter(date, formats) { return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1]; } @@ -14814,16 +17074,18 @@ var DATE_FORMATS = { EEEE: dateStrGetter('Day'), EEE: dateStrGetter('Day', true), a: ampmGetter, - Z: timeZoneGetter + Z: timeZoneGetter, + ww: weekGetter(2), + w: weekGetter(1) }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/, +var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/, NUMBER_STRING = /^\-?\d+$/; /** * @ngdoc filter * @name date - * @function + * @kind function * * @description * Formats `date` to a string based on the requested `format`. @@ -14843,40 +17105,44 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ * * `'EEE'`: Day in Week, (Sun-Sat) * * `'HH'`: Hour in day, padded (00-23) * * `'H'`: Hour in day (0-23) - * * `'hh'`: Hour in am/pm, padded (01-12) - * * `'h'`: Hour in am/pm, (1-12) + * * `'hh'`: Hour in AM/PM, padded (01-12) + * * `'h'`: Hour in AM/PM, (1-12) * * `'mm'`: Minute in hour, padded (00-59) * * `'m'`: Minute in hour (0-59) * * `'ss'`: Second in minute, padded (00-59) * * `'s'`: Second in minute (0-59) * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999) - * * `'a'`: am/pm marker + * * `'a'`: AM/PM marker * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200) + * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year + * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year * * `format` string can also be one of the following predefined * {@link guide/i18n localizable formats}: * * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale - * (e.g. Sep 3, 2010 12:05:08 pm) - * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm) - * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale + * (e.g. Sep 3, 2010 12:05:08 PM) + * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM) + * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale * (e.g. Friday, September 3, 2010) * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010) * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010) * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10) - * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm) - * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm) + * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM) + * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM) * - * `format` string can contain literal values. These need to be quoted with single quotes (e.g. - * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence + * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g. + * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence * (e.g. `"h 'o''clock'"`). * * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or - * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its + * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is * 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. + * 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. * * @example @@ -14888,6 +17154,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ <span>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span><br> <span ng-non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>: <span>{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}</span><br> + <span ng-non-bindable>{{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}}</span>: + <span>{{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}</span><br> </file> <file name="protractor.js" type="protractor"> it('should format date', function() { @@ -14897,6 +17165,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+ toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); + expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). + toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/); }); </file> </example> @@ -14921,10 +17191,10 @@ function dateFilter($locale) { tzMin = int(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); - var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000); + var h = int(match[4] || 0) - tzHour; + var m = int(match[5] || 0) - tzMin; + var s = int(match[6] || 0); + var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000); timeSetter.call(date, h, m, s, ms); return date; } @@ -14932,7 +17202,7 @@ function dateFilter($locale) { } - return function(date, format) { + return function(date, format, timezone) { var text = '', parts = [], fn, match; @@ -14940,11 +17210,7 @@ function dateFilter($locale) { format = format || 'mediumDate'; format = $locale.DATETIME_FORMATS[format] || format; if (isString(date)) { - if (NUMBER_STRING.test(date)) { - date = int(date); - } else { - date = jsonStringToDate(date); - } + date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date); } if (isNumber(date)) { @@ -14955,7 +17221,7 @@ function dateFilter($locale) { return date; } - while(format) { + while (format) { match = DATE_FORMATS_SPLIT.exec(format); if (match) { parts = concat(parts, match, 1); @@ -14966,7 +17232,11 @@ function dateFilter($locale) { } } - forEach(parts, function(value){ + if (timezone && timezone === 'UTC') { + date = new Date(date.getTime()); + date.setMinutes(date.getMinutes() + date.getTimezoneOffset()); + } + forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS) : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); @@ -14980,7 +17250,7 @@ function dateFilter($locale) { /** * @ngdoc filter * @name json - * @function + * @kind function * * @description * Allows you to convert a JavaScript object into JSON string. @@ -14989,25 +17259,31 @@ function dateFilter($locale) { * the binding is automatically converted to JSON. * * @param {*} object Any JavaScript object (including arrays and primitive types) to filter. + * @param {number=} spacing The number of spaces to use per indentation, defaults to 2. * @returns {string} JSON string. * * * @example <example> <file name="index.html"> - <pre>{{ {'name':'value'} | json }}</pre> + <pre id="default-spacing">{{ {'name':'value'} | json }}</pre> + <pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre> </file> <file name="protractor.js" type="protractor"> it('should jsonify filtered objects', function() { - expect(element(by.binding("{'name':'value'}")).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); }); </file> </example> * */ function jsonFilter() { - return function(object) { - return toJson(object, true); + return function(object, spacing) { + if (isUndefined(spacing)) { + spacing = 2; + } + return toJson(object, spacing); }; } @@ -15015,7 +17291,7 @@ function jsonFilter() { /** * @ngdoc filter * @name lowercase - * @function + * @kind function * @description * Converts string to lowercase. * @see angular.lowercase @@ -15026,7 +17302,7 @@ var lowercaseFilter = valueFn(lowercase); /** * @ngdoc filter * @name uppercase - * @function + * @kind function * @description * Converts string to uppercase. * @see angular.uppercase @@ -15036,14 +17312,15 @@ var uppercaseFilter = valueFn(uppercase); /** * @ngdoc filter * @name limitTo - * @function + * @kind function * * @description * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array or string, as specified by - * the value and sign (positive or negative) of `limit`. + * are taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. If a number is used as input, it is + * converted to a string. * - * @param {Array|string} input Source array or string to be limited. + * @param {Array|string|number} input Source array, string or number to be limited. * @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 @@ -15052,61 +17329,82 @@ var uppercaseFilter = valueFn(uppercase); * had less than `limit` elements. * * @example - <example> + <example module="limitToExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.numbers = [1,2,3,4,5,6,7,8,9]; - $scope.letters = "abcdefghi"; - $scope.numLimit = 3; - $scope.letterLimit = 3; - } + angular.module('limitToExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.numbers = [1,2,3,4,5,6,7,8,9]; + $scope.letters = "abcdefghi"; + $scope.longNumber = 2345432342; + $scope.numLimit = 3; + $scope.letterLimit = 3; + $scope.longNumberLimit = 3; + }]); </script> - <div ng-controller="Ctrl"> - Limit {{numbers}} to: <input type="integer" ng-model="numLimit"> + <div ng-controller="ExampleController"> + Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit"> <p>Output numbers: {{ numbers | limitTo:numLimit }}</p> - Limit {{letters}} to: <input type="integer" ng-model="letterLimit"> + Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit"> <p>Output letters: {{ letters | limitTo:letterLimit }}</p> + Limit {{longNumber}} to: <input type="number" step="1" ng-model="longNumberLimit"> + <p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p> </div> </file> <file name="protractor.js" type="protractor"> var numLimitInput = element(by.model('numLimit')); var letterLimitInput = element(by.model('letterLimit')); + var longNumberLimitInput = element(by.model('longNumberLimit')); var limitedNumbers = element(by.binding('numbers | limitTo:numLimit')); var limitedLetters = element(by.binding('letters | limitTo:letterLimit')); + var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit')); it('should limit the number array to first three items', function() { expect(numLimitInput.getAttribute('value')).toBe('3'); expect(letterLimitInput.getAttribute('value')).toBe('3'); + expect(longNumberLimitInput.getAttribute('value')).toBe('3'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]'); expect(limitedLetters.getText()).toEqual('Output letters: abc'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 234'); }); - it('should update the output when -3 is entered', function() { - numLimitInput.clear(); - numLimitInput.sendKeys('-3'); - letterLimitInput.clear(); - letterLimitInput.sendKeys('-3'); - expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); - expect(limitedLetters.getText()).toEqual('Output letters: ghi'); - }); + // There is a bug in safari and protractor that doesn't like the minus key + // it('should update the output when -3 is entered', function() { + // numLimitInput.clear(); + // numLimitInput.sendKeys('-3'); + // letterLimitInput.clear(); + // letterLimitInput.sendKeys('-3'); + // longNumberLimitInput.clear(); + // longNumberLimitInput.sendKeys('-3'); + // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]'); + // expect(limitedLetters.getText()).toEqual('Output letters: ghi'); + // expect(limitedLongNumber.getText()).toEqual('Output long number: 342'); + // }); it('should not exceed the maximum size of input array', function() { numLimitInput.clear(); numLimitInput.sendKeys('100'); letterLimitInput.clear(); letterLimitInput.sendKeys('100'); + longNumberLimitInput.clear(); + longNumberLimitInput.sendKeys('100'); expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]'); expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi'); + expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342'); }); </file> </example> - */ -function limitToFilter(){ +*/ +function limitToFilter() { return function(input, limit) { + if (isNumber(input)) input = input.toString(); if (!isArray(input) && !isString(input)) return input; - limit = int(limit); + if (Math.abs(Number(limit)) === Infinity) { + limit = Number(limit); + } else { + limit = int(limit); + } if (isString(input)) { //NaN check on limit @@ -15134,7 +17432,7 @@ function limitToFilter(){ n = input.length; } - for (; i<n; i++) { + for (; i < n; i++) { out.push(input[i]); } @@ -15145,43 +17443,53 @@ function limitToFilter(){ /** * @ngdoc filter * @name orderBy - * @function + * @kind function * * @description - * Orders a specified `array` by the `expression` predicate. + * 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. * * @param {Array} array The array to sort. - * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be * used by the comparator to determine the order of elements. * * Can be one of: * * - `function`: Getter function. The result of this function will be sorted using the * `<`, `=`, `>` operator. - * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' - * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control - * ascending or descending sort order (for example, +name or -name). + * - `string`: An Angular expression. The result of this expression is used to compare elements + * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by + * 3 first characters of a property called `name`). The result of a constant expression + * is interpreted as a property name to be used in comparisons (for example `"special name"` + * to sort object by the value of their `special name` property). An expression can be + * optionally prefixed with `+` or `-` to control ascending or descending sort order + * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array + * element itself is used to compare where sorting. * - `Array`: An array of function or string predicates. The first predicate in the array * is used for sorting, but when two items are equivalent, the next predicate is used. * + * If the predicate is missing or empty then it defaults to `'+'`. + * * @param {boolean=} reverse Reverse the order of the array. * @returns {Array} Sorted copy of the source array. * * @example - <example> + <example module="orderByExample"> <file name="index.html"> <script> - function Ctrl($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'; - } + 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="Ctrl"> + <div ng-controller="ExampleController"> <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> <hr/> [ <a href="" ng-click="predicate=''">unsorted</a> ] @@ -15201,53 +17509,131 @@ function limitToFilter(){ </div> </file> </example> + * + * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the + * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the + * desired parameters. + * + * Example: + * + * @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> + </file> + + <file name="script.js"> + angular.module('orderByExample', []) + .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { + var orderBy = $filter('orderBy'); + $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.order = function(predicate, reverse) { + $scope.friends = orderBy($scope.friends, predicate, reverse); + }; + $scope.order('-age',false); + }]); + </file> +</example> */ orderByFilter.$inject = ['$parse']; -function orderByFilter($parse){ +function orderByFilter($parse) { return function(array, sortPredicate, reverseOrder) { - if (!isArray(array)) return array; - if (!sortPredicate) return array; - sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; - sortPredicate = map(sortPredicate, function(predicate){ + if (!(isArrayLike(array))) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate]; + if (sortPredicate.length === 0) { sortPredicate = ['+']; } + sortPredicate = sortPredicate.map(function(predicate) { var descending = false, get = predicate || identity; if (isString(predicate)) { if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { descending = predicate.charAt(0) == '-'; predicate = predicate.substring(1); } + if (predicate === '') { + // Effectively no predicate was passed so we compare identity + return reverseComparator(function(a, b) { + return compare(a, b); + }, descending); + } get = $parse(predicate); if (get.constant) { var key = get(); - return reverseComparator(function(a,b) { + return reverseComparator(function(a, b) { return compare(a[key], b[key]); }, descending); } } - return reverseComparator(function(a,b){ + return reverseComparator(function(a, b) { return compare(get(a),get(b)); }, descending); }); - var arrayCopy = []; - for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } - return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + return slice.call(array).sort(reverseComparator(comparator, reverseOrder)); - function comparator(o1, o2){ - for ( var i = 0; i < sortPredicate.length; i++) { + 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 toBoolean(descending) - ? function(a,b){return comp(b,a);} + return descending + ? function(a, b) {return comp(b,a);} : comp; } - function compare(v1, v2){ + + function isPrimitive(value) { + switch (typeof value) { + case 'number': /* falls through */ + case 'boolean': /* falls through */ + case 'string': + return true; + default: + return false; + } + } + + function objectToString(value) { + if (value === null) return 'null'; + if (typeof value.toString === 'function') { + value = value.toString(); + if (isPrimitive(value)) return value; + } + if (typeof value.valueOf === 'function') { + value = value.valueOf(); + if (isPrimitive(value)) return value; + } + return ''; + } + + function compare(v1, v2) { var t1 = typeof v1; var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") { + if (t1 === t2 && t1 === "object") { + v1 = objectToString(v1); + v2 = objectToString(v2); + } + if (t1 === t2) { + if (t1 === "string") { v1 = v1.toLowerCase(); v2 = v2.toLowerCase(); } @@ -15286,28 +17672,12 @@ function ngDirective(directive) { var htmlAnchorDirective = valueFn({ restrict: 'E', compile: function(element, attr) { - - if (msie <= 8) { - - // turn <a href ng-click="..">link</a> into a stylable link in IE - // but only if it doesn't have name attribute, in which case it's an anchor - if (!attr.href && !attr.name) { - attr.$set('href', ''); - } - - // add a comment node to anchors to workaround IE bug that causes element content to be reset - // to new attribute content if attribute is updated with value containing @ and element also - // contains value with @ - // see issue #1949 - element.append(document.createComment('IE fix')); - } - if (!attr.href && !attr.xlinkHref && !attr.name) { return function(scope, element) { // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute. var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ? 'xlink:href' : 'href'; - element.on('click', function(event){ + element.on('click', function(event) { // if we have no href url, then don't navigate anywhere. if (!element.attr(href)) { event.preventDefault(); @@ -15329,18 +17699,17 @@ var htmlAnchorDirective = valueFn({ * make the link go to the wrong URL if the user clicks it before * Angular has a chance to replace the `{{hash}}` markup with its * value. Until Angular replaces the markup the link will be broken - * and will most likely return a 404 error. - * - * The `ngHref` directive solves this problem. + * and will most likely return a 404 error. The `ngHref` directive + * solves this problem. * * The wrong way to write it: * ```html - * <a href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * The correct way to write it: * ```html - * <a ng-href="http://www.gravatar.com/avatar/{{hash}}"/> + * <a ng-href="http://www.gravatar.com/avatar/{{hash}}">link1</a> * ``` * * @element A @@ -15384,7 +17753,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/123$/); }); - }, 1000, 'page should navigate to /123'); + }, 5000, 'page should navigate to /123'); }); xit('should execute ng-click but not reload when href empty string and name specified', function() { @@ -15412,7 +17781,7 @@ var htmlAnchorDirective = valueFn({ return browser.driver.getCurrentUrl().then(function(url) { return url.match(/\/6$/); }); - }, 1000, 'page should navigate to /6'); + }, 5000, 'page should navigate to /6'); }); </file> </example> @@ -15478,7 +17847,7 @@ var htmlAnchorDirective = valueFn({ * * @description * - * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: + * We shouldn't do this, because it will make the button enabled on Chrome/Firefox but not on IE8 and older IEs: * ```html * <div ng-init="scope = { isDisabled: false }"> * <button disabled="{{scope.isDisabled}}">Disabled</button> @@ -15669,6 +18038,7 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { var normalized = directiveNormalize('ng-' + attrName); ngAttributeAliasDirectives[normalized] = function() { return { + restrict: 'A', priority: 100, link: function(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -15679,6 +18049,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) { }; }); +// aliased input attrs are evaluated +forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { + ngAttributeAliasDirectives[ngAttr] = function() { + return { + priority: 100, + link: function(scope, element, attr) { + //special case ngPattern when a literal regular expression value + //is used as the expression (this way we don't have to watch anything). + if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + var match = attr.ngPattern.match(REGEX_STRING_REGEXP); + if (match) { + attr.$set("ngPattern", new RegExp(match[1], match[2])); + return; + } + } + + scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) { + attr.$set(ngAttr, value); + }); + } + }; + }; +}); // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { @@ -15698,8 +18091,12 @@ forEach(['src', 'srcset', 'href'], function(attrName) { } attr.$observe(normalized, function(value) { - if (!value) - return; + if (!value) { + if (attrName === 'href') { + attr.$set(name, null); + } + return; + } attr.$set(name, value); @@ -15714,14 +18111,22 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl */ +/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true + */ var nullFormCtrl = { $addControl: noop, + $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, - $setPristine: noop -}; + $setPristine: noop, + $setSubmitted: noop +}, +SUBMITTED_CLASS = 'ng-submitted'; + +function nullFormRenameControl(control, name) { + control.$name = name; +} /** * @ngdoc type @@ -15731,13 +18136,13 @@ var nullFormCtrl = { * @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} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to all invalid controls or - * forms, where: + * @property {Object} $error Is an object hash, containing references to controls or + * forms with failing validators, where: * * - keys are validation tokens (error names), - * - values are arrays of controls or forms that are invalid for given error name. - * + * - values are arrays of controls or forms that have a failing validator for given error name. * * Built-in validation tokens: * @@ -15750,9 +18155,14 @@ var nullFormCtrl = { * - `pattern` * - `required` * - `url` + * - `date` + * - `datetimelocal` + * - `time` + * - `week` + * - `month` * * @description - * `FormController` keeps track of all its controls and nested forms as well as state of them, + * `FormController` keeps track of all its controls and nested forms as well as the state of them, * such as being valid/invalid or dirty/pristine. * * Each {@link ng.directive:form form} directive creates an instance @@ -15760,33 +18170,59 @@ var nullFormCtrl = { * */ //asks for $scope to fool the BC controller module -FormController.$inject = ['$element', '$attrs', '$scope', '$animate']; -function FormController(element, attrs, $scope, $animate) { +FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; +function FormController(element, attrs, $scope, $animate, $interpolate) { var form = this, - parentForm = element.parent().controller('form') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - errors = form.$error = {}, controls = []; + var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl; + // init state - form.$name = attrs.name || attrs.ngForm; + form.$error = {}; + form.$$success = {}; + form.$pending = undefined; + form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); form.$dirty = false; form.$pristine = true; form.$valid = true; form.$invalid = false; + form.$submitted = false; parentForm.$addControl(form); - // Setup initial state of the control - element.addClass(PRISTINE_CLASS); - toggleValidCss(true); + /** + * @ngdoc method + * @name form.FormController#$rollbackViewValue + * + * @description + * Rollback all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is typically needed by the reset button of + * a form that uses `ng-model-options` to pend updates. + */ + form.$rollbackViewValue = function() { + forEach(controls, function(control) { + control.$rollbackViewValue(); + }); + }; - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass(element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass(element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + /** + * @ngdoc method + * @name form.FormController#$commitViewValue + * + * @description + * Commit all form controls pending updates to the `$modelValue`. + * + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. + */ + form.$commitViewValue = function() { + forEach(controls, function(control) { + control.$commitViewValue(); + }); + }; /** * @ngdoc method @@ -15808,6 +18244,17 @@ function FormController(element, attrs, $scope, $animate) { } }; + // Private API: rename a form control + form.$$renameControl = function(control, newName) { + var oldName = control.$name; + + if (form[oldName] === control) { + delete form[oldName]; + } + form[newName] = control; + control.$name = newName; + }; + /** * @ngdoc method * @name form.FormController#$removeControl @@ -15821,13 +18268,17 @@ function FormController(element, attrs, $scope, $animate) { if (control.$name && form[control.$name] === control) { delete form[control.$name]; } - forEach(errors, function(queue, validationToken) { - form.$setValidity(validationToken, true, control); + forEach(form.$pending, function(value, name) { + form.$setValidity(name, null, control); + }); + forEach(form.$error, function(value, name) { + form.$setValidity(name, null, control); }); arrayRemove(controls, control); }; + /** * @ngdoc method * @name form.FormController#$setValidity @@ -15837,43 +18288,33 @@ function FormController(element, attrs, $scope, $animate) { * * This method will also propagate to parent forms. */ - form.$setValidity = function(validationToken, isValid, control) { - var queue = errors[validationToken]; - - if (isValid) { - if (queue) { - arrayRemove(queue, control); - if (!queue.length) { - invalidCount--; - if (!invalidCount) { - toggleValidCss(isValid); - form.$valid = true; - form.$invalid = false; - } - errors[validationToken] = false; - toggleValidCss(true, validationToken); - parentForm.$setValidity(validationToken, true, form); + addSetValidityMethod({ + ctrl: this, + $element: element, + set: function(object, property, control) { + var list = object[property]; + if (!list) { + object[property] = [control]; + } else { + var index = list.indexOf(control); + if (index === -1) { + list.push(control); } } - - } else { - if (!invalidCount) { - toggleValidCss(isValid); + }, + unset: function(object, property, control) { + var list = object[property]; + if (!list) { + return; } - if (queue) { - if (includes(queue, control)) return; - } else { - errors[validationToken] = queue = []; - invalidCount++; - toggleValidCss(false, validationToken); - parentForm.$setValidity(validationToken, false, form); + arrayRemove(list, control); + if (list.length === 0) { + delete object[property]; } - queue.push(control); - - form.$valid = false; - form.$invalid = true; - } - }; + }, + parentForm: parentForm, + $animate: $animate + }); /** * @ngdoc method @@ -15907,17 +18348,48 @@ function FormController(element, attrs, $scope, $animate) { * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function () { - $animate.removeClass(element, DIRTY_CLASS); - $animate.addClass(element, PRISTINE_CLASS); + form.$setPristine = function() { + $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); form.$dirty = false; form.$pristine = true; + form.$submitted = false; forEach(controls, function(control) { control.$setPristine(); }); }; -} + /** + * @ngdoc method + * @name form.FormController#$setUntouched + * + * @description + * Sets the form to its untouched state. + * + * This method can be called to remove the 'ng-touched' class and set the form controls to their + * untouched state (ng-untouched class). + * + * Setting a form controls back to their untouched state is often useful when setting the form + * back to its pristine state. + */ + form.$setUntouched = function() { + forEach(controls, function(control) { + control.$setUntouched(); + }); + }; + + /** + * @ngdoc method + * @name form.FormController#$setSubmitted + * + * @description + * Sets the form to its submitted state. + */ + form.$setSubmitted = function() { + $animate.addClass(element, SUBMITTED_CLASS); + form.$submitted = true; + parentForm.$setSubmitted(); + }; +} /** * @ngdoc directive @@ -15967,6 +18439,7 @@ function FormController(element, attrs, $scope, $animate) { * - `ng-invalid` is set if the form is invalid. * - `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. * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -16000,8 +18473,9 @@ function FormController(element, attrs, $scope, $animate) { * hitting enter in any of the input fields will trigger the click handler on the *first* button or * input[type=submit] (`ngClick`) *and* a submit handler on the enclosing form (`ngSubmit`) * - * @param {string=} name Name of the form. If specified, the form controller will be published into - * related scope, under this name. + * Any pending `ngModelOptions` changes will take place immediately when an enclosing form is + * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. * * ## Animation Hooks * @@ -16028,12 +18502,13 @@ function FormController(element, attrs, $scope, $animate) { * </pre> * * @example - <example deps="angular-animate.js" animations="true" fixBase="true"> + <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.userType = 'guest'; - } + angular.module('formExample', []) + .controller('FormController', ['$scope', function($scope) { + $scope.userType = 'guest'; + }]); </script> <style> .my-form { @@ -16045,7 +18520,7 @@ function FormController(element, attrs, $scope, $animate) { background: red; } </style> - <form name="myForm" ng-controller="Ctrl" class="my-form"> + <form name="myForm" ng-controller="FormController" class="my-form"> userType: <input name="input" ng-model="userType" required> <span class="error" ng-show="myForm.input.$error.required">Required!</span><br> <tt>userType = {{userType}}</tt><br> @@ -16078,6 +18553,8 @@ function FormController(element, attrs, $scope, $animate) { </file> </example> * + * @param {string=} name Name of the form. If specified, the form controller will be published into + * related scope, under this name. */ var formDirectiveFactory = function(isNgForm) { return ['$timeout', function($timeout) { @@ -16085,48 +18562,60 @@ var formDirectiveFactory = function(isNgForm) { name: 'form', restrict: isNgForm ? 'EAC' : 'E', controller: FormController, - compile: function() { + compile: function ngFormCompile(formElement) { + // Setup initial state of the control + formElement.addClass(PRISTINE_CLASS).addClass(VALID_CLASS); + return { - pre: function(scope, formElement, attr, controller) { - if (!attr.action) { + pre: function ngFormPreLink(scope, formElement, attr, controller) { + // 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 // action is not prevented. see #1238 // // IE 9 is not affected because it doesn't fire a submit event and try to do a full // page reload if the form was destroyed by submission of the form via a click handler // on a button in the form. Looks like an IE9 specific bug. - var preventDefaultListener = function(event) { - event.preventDefault - ? event.preventDefault() - : event.returnValue = false; // IE + var handleFormSubmission = function(event) { + scope.$apply(function() { + controller.$commitViewValue(); + controller.$setSubmitted(); + }); + + event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', preventDefaultListener); + addEventListenerFn(formElement[0], 'submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', preventDefaultListener); + removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); }, 0, false); }); } - var parentFormCtrl = formElement.parent().controller('form'), - alias = attr.name || attr.ngForm; + var parentFormCtrl = controller.$$parentForm, + alias = controller.$name; if (alias) { setter(scope, alias, controller, alias); - } - if (parentFormCtrl) { - formElement.on('$destroy', function() { - parentFormCtrl.$removeControl(controller); - if (alias) { - setter(scope, alias, undefined, alias); - } - extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) { + if (alias === newValue) return; + setter(scope, alias, undefined, alias); + alias = newValue; + setter(scope, alias, controller, alias); + parentFormCtrl.$$renameControl(controller, alias); }); } + formElement.on('$destroy', function() { + parentFormCtrl.$removeControl(controller); + if (alias) { + setter(scope, alias, undefined, alias); + } + extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards + }); } }; } @@ -16139,17 +18628,27 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global - - -VALID_CLASS, - -INVALID_CLASS, - -PRISTINE_CLASS, - -DIRTY_CLASS +/* global VALID_CLASS: true, + INVALID_CLASS: true, + PRISTINE_CLASS: true, + DIRTY_CLASS: true, + UNTOUCHED_CLASS: true, + TOUCHED_CLASS: 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#!:.?+=&%@!\-\/]))?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/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 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 DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; + +var $ngModelMinErr = new minErr('ngModel'); var inputType = { @@ -16158,7 +18657,8 @@ var inputType = { * @name input[text] * * @description - * Standard HTML text input with angular data binding. + * Standard HTML text input with angular data binding, inherited by most of the `input` 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. @@ -16169,24 +18669,33 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @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. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - <example name="text-input-directive"> + <example name="text-input-directive" module="textInputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'guest'; - $scope.word = /^\s*\w*\s*$/; - } + angular.module('textInputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'guest'; + $scope.word = /^\s*\w*\s*$/; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Single word: <input type="text" name="input" ng-model="text" ng-pattern="word" required ng-trim="false"> <span class="error" ng-show="myForm.input.$error.required"> @@ -16230,6 +18739,463 @@ var inputType = { */ 'text': textInputType, + /** + * @ngdoc input + * @name input[date] + * + * @description + * Input with date validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601 + * date format (yyyy-MM-dd), for example: `2009-01-06`. Since many + * modern browsers do not yet support this input type, it is important to provide cues to users on the + * expected input format via a placeholder or label. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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). + * @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). + * @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 input changes due to user + * interaction with the input element. + * + * @example + <example name="date-input-directive" module="dateInputExample"> + <file name="index.html"> + <script> + angular.module('dateInputExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 22); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date in 2013: + <input type="date" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-dd"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (see https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10-22'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'date': createDateInputType('date', DATE_REGEXP, + createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']), + 'yyyy-MM-dd'), + + /** + * @ngdoc input + * @name input[datetime-local] + * + * @description + * Input with datetime validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="datetimelocal-input-directive" module="dateExample"> + <file name="index.html"> + <script> + angular.module('dateExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2010, 11, 28, 14, 57); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input type="datetime-local" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2010-12-28T14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01-01T23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[time] + * + * @description + * Input with time validation and transformation. In browsers that do not yet support + * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a + * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="time-input-directive" module="timeExample"> + <file name="index.html"> + <script> + angular.module('timeExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(1970, 0, 1, 14, 57, 0); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a between 8am and 5pm: + <input type="time" id="exampleInput" name="input" ng-model="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> + <tt>value = {{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/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "HH:mm:ss"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('14:57:00'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('23:59:00'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'time': createDateInputType('time', TIME_REGEXP, + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), + + /** + * @ngdoc input + * @name input[week] + * + * @description + * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support + * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * week format (yyyy-W##), for example: `2013-W02`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="week-input-directive" module="weekExample"> + <file name="index.html"> + <script> + angular.module('weekExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 0, 3); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a date between in 2013: + <input id="exampleInput" type="week" name="input" ng-model="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> + <tt>value = {{value | date: "yyyy-Www"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-Www"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-W01'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-W01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'), + + /** + * @ngdoc input + * @name input[month] + * + * @description + * Input with month validation and transformation. In browsers that do not yet support + * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601 + * month format (yyyy-MM), for example: `2009-01`. + * + * The model must always be a Date object, otherwise Angular will throw an error. + * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. + * If the model is not set to the first of the month, the next view to model update will set it + * to the first of the month. + * + * The timezone to be used to read/write the `Date` instance in the model can be defined using + * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser. + * + * @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=} 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 input changes due to user + * interaction with the input element. + * + * @example + <example name="month-input-directive" module="monthExample"> + <file name="index.html"> + <script> + angular.module('monthExample', []) + .controller('DateController', ['$scope', function($scope) { + $scope.value = new Date(2013, 9, 1); + }]); + </script> + <form name="myForm" ng-controller="DateController as dateCtrl"> + Pick a month int 2013: + <input id="exampleInput" type="month" name="input" ng-model="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> + <tt>value = {{value | date: "yyyy-MM"}}</tt><br/> + <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> + <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> + <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + </form> + </file> + <file name="protractor.js" type="protractor"> + var value = element(by.binding('value | date: "yyyy-MM"')); + var valid = element(by.binding('myForm.input.$valid')); + var input = element(by.model('value')); + + // currently protractor/webdriver does not support + // sending keys to all known HTML5 input controls + // for various browsers (https://github.com/angular/protractor/issues/562). + function setInput(val) { + // set the value of the element and force validation. + var scr = "var ipt = document.getElementById('exampleInput'); " + + "ipt.value = '" + val + "';" + + "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });"; + browser.executeScript(scr); + } + + it('should initialize to model', function() { + expect(value.getText()).toContain('2013-10'); + expect(valid.getText()).toContain('myForm.input.$valid = true'); + }); + + it('should be invalid if empty', function() { + setInput(''); + expect(value.getText()).toEqual('value ='); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + + it('should be invalid if over max', function() { + setInput('2015-01'); + expect(value.getText()).toContain(''); + expect(valid.getText()).toContain('myForm.input.$valid = false'); + }); + </file> + </example> + */ + 'month': createDateInputType('month', MONTH_REGEXP, + createDateParser(MONTH_REGEXP, ['yyyy', 'MM']), + 'yyyy-MM'), /** * @ngdoc input @@ -16239,6 +19205,8 @@ var inputType = { * Text input with number validation and transformation. Sets the `number` validation * error if not a valid number. * + * The model must always be a number, otherwise Angular will throw an error. + * * @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`. @@ -16250,22 +19218,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="number-input-directive"> + <example name="number-input-directive" module="numberExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value = 12; - } + angular.module('numberExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 12; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Number: <input type="number" name="input" ng-model="value" min="0" max="99" required> <span class="error" ng-show="myForm.input.$error.required"> @@ -16316,6 +19291,12 @@ var inputType = { * Text input with URL validation. Sets the `url` validation error key if the content is not a * valid URL. * + * <div class="alert alert-warning"> + * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex + * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify + * the built-in validators (see the {@link guide/forms Forms guide}) + * </div> + * * @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 Sets `required` validation error key if the value is not entered. @@ -16325,22 +19306,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="url-input-directive"> + <example name="url-input-directive" module="urlExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'http://google.com'; - } + angular.module('urlExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'http://google.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> URL: <input type="url" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -16392,6 +19380,12 @@ var inputType = { * Text input with email validation. Sets the `email` validation error key if not a valid email * address. * + * <div class="alert alert-warning"> + * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex + * used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can + * use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide}) + * </div> + * * @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 Sets `required` validation error key if the value is not entered. @@ -16401,22 +19395,29 @@ var inputType = { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. - * @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. + * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of + * any length. + * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string + * that contains the regular expression body that will be converted to a regular expression + * 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$')`. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="email-input-directive"> + <example name="email-input-directive" module="emailExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.text = 'me@example.com'; - } + angular.module('emailExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.text = 'me@example.com'; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Email: <input type="email" name="input" ng-model="text" required> <span class="error" ng-show="myForm.input.$error.required"> Required!</span> @@ -16475,18 +19476,19 @@ var inputType = { * be set when selected. * * @example - <example name="radio-input-directive"> + <example name="radio-input-directive" module="radioExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.color = 'blue'; - $scope.specialValue = { - "id": "12345", - "value": "green" - }; - } + angular.module('radioExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.color = 'blue'; + $scope.specialValue = { + "id": "12345", + "value": "green" + }; + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> <input type="radio" ng-model="color" value="red"> Red <br/> <input type="radio" ng-model="color" ng-value="specialValue"> Green <br/> <input type="radio" ng-model="color" value="blue"> Blue <br/> @@ -16519,24 +19521,25 @@ 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=} ngTrueValue The value to which the expression should be set when selected. - * @param {string=} ngFalseValue The value to which the expression should be set when not selected. + * @param {expression=} ngTrueValue The value to which the expression should be set when selected. + * @param {expression=} ngFalseValue The value to which the expression should be set when not selected. * @param {string=} ngChange Angular expression to be executed when input changes due to user * interaction with the input element. * * @example - <example name="checkbox-input-directive"> + <example name="checkbox-input-directive" module="checkboxExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.value1 = true; - $scope.value2 = 'YES' - } + angular.module('checkboxExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value1 = true; + $scope.value2 = 'YES' + }]); </script> - <form name="myForm" ng-controller="Ctrl"> + <form name="myForm" ng-controller="ExampleController"> Value1: <input type="checkbox" ng-model="value1"> <br/> Value2: <input type="checkbox" ng-model="value2" - ng-true-value="YES" ng-false-value="NO"> <br/> + ng-true-value="'YES'" ng-false-value="'NO'"> <br/> <tt>value1 = {{value1}}</tt><br/> <tt>value2 = {{value2}}</tt><br/> </form> @@ -16567,33 +19570,20 @@ var inputType = { 'file': noop }; -// A helper function to call $setValidity and return the value / undefined, -// a pattern that is repeated a lot in the input validation logic. -function validate(ctrl, validatorName, validity, value){ - ctrl.$setValidity(validatorName, validity); - return validity ? value : undefined; +function stringBasedInputType(ctrl) { + ctrl.$formatters.push(function(value) { + return ctrl.$isEmpty(value) ? value : value.toString(); + }); } - -function addNativeHtml5Validators(ctrl, validatorName, element) { - var validity = element.prop('validity'); - if (isObject(validity)) { - var validator = function(value) { - // Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can - // perform the required validation) - if (!ctrl.$error[validatorName] && (validity.badInput || validity.customError || - validity.typeMismatch) && !validity.valueMissing) { - ctrl.$setValidity(validatorName, false); - return; - } - return value; - }; - ctrl.$parsers.push(validator); - } +function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); } -function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { - var validity = element.prop('validity'); +function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { + var type = lowercase(element[0].type); + // In composition mode, users are still inputing intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent @@ -16610,29 +19600,27 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } - var listener = function() { + var listener = function(ev) { + if (timeout) { + $browser.defer.cancel(timeout); + timeout = null; + } if (composing) return; - var value = element.val(); + var value = element.val(), + event = ev && ev.type; // By default we will trim the value // If the attribute ng-trim exists we will avoid trimming - // e.g. <input ng-model="foo" ng-trim="false"> - if (toBoolean(attr.ngTrim || 'T')) { + // If input type is 'password', the value is never trimmed + if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) { value = trim(value); } - if (ctrl.$viewValue !== value || - // If the value is still empty/falsy, and there is no `required` error, run validators - // again. This enables HTML5 constraint validation errors to affect Angular validation - // even when the first character entered causes an error. - (validity && value === '' && !validity.valueMissing)) { - if (scope.$$phase) { - ctrl.$setViewValue(value); - } else { - scope.$apply(function() { - ctrl.$setViewValue(value); - }); - } + // If a control is suffering from bad input (due to native validators), browsers discard its + // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the + // control's value is the same empty value twice in a row. + if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) { + ctrl.$setViewValue(value, event); } }; @@ -16643,11 +19631,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { } else { var timeout; - var deferListener = function() { + var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { - listener(); timeout = null; + if (!input || input.value !== origValue) { + listener(ev); + } }); } }; @@ -16659,7 +19649,7 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; - deferListener(); + deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it @@ -16675,127 +19665,256 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { ctrl.$render = function() { element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue); }; +} - // pattern validator - var pattern = attr.ngPattern, - patternValidator, - match; +function weekParser(isoWeek, existingDate) { + if (isDate(isoWeek)) { + return isoWeek; + } - if (pattern) { - var validateRegex = function(regexp, value) { - return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value); - }; - match = pattern.match(/^\/(.*)\/([gim]*)$/); - if (match) { - pattern = new RegExp(match[1], match[2]); - patternValidator = function(value) { - return validateRegex(pattern, value); - }; - } else { - patternValidator = function(value) { - var patternObj = scope.$eval(pattern); + if (isString(isoWeek)) { + WEEK_REGEXP.lastIndex = 0; + var parts = WEEK_REGEXP.exec(isoWeek); + if (parts) { + var year = +parts[1], + week = +parts[2], + hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0, + firstThurs = getFirstThursdayOfYear(year), + addDays = (week - 1) * 7; + + if (existingDate) { + hours = existingDate.getHours(); + minutes = existingDate.getMinutes(); + seconds = existingDate.getSeconds(); + milliseconds = existingDate.getMilliseconds(); + } - if (!patternObj || !patternObj.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern, - patternObj, startingTag(element)); + return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds); + } + } + + return NaN; +} + +function createDateParser(regexp, mapping) { + return function(iso, date) { + var parts, map; + + if (isDate(iso)) { + return iso; + } + + if (isString(iso)) { + // When a date is JSON'ified to wraps itself inside of an extra + // set of double quotes. This makes the date parsing code unable + // to match the date string and parse it as a date. + if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { + iso = iso.substring(1, iso.length - 1); + } + if (ISO_DATE_REGEXP.test(iso)) { + return new Date(iso); + } + regexp.lastIndex = 0; + parts = regexp.exec(iso); + + if (parts) { + parts.shift(); + if (date) { + map = { + yyyy: date.getFullYear(), + MM: date.getMonth() + 1, + dd: date.getDate(), + HH: date.getHours(), + mm: date.getMinutes(), + ss: date.getSeconds(), + sss: date.getMilliseconds() / 1000 + }; + } else { + map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; } - return validateRegex(patternObj, value); - }; + + forEach(parts, function(part, index) { + if (index < mapping.length) { + map[mapping[index]] = +part; + } + }); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + } } - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } + return NaN; + }; +} - // min length validator - if (attr.ngMinlength) { - var minlength = int(attr.ngMinlength); - var minLengthValidator = function(value) { - return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value); - }; +function createDateInputType(type, regexp, parseDate, format) { + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var previousDate; + + ctrl.$$parserName = type; + ctrl.$parsers.push(function(value) { + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { + // Note: We cannot read ctrl.$modelValue, as there might be a different + // 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()); + } + return parsedDate; + } + return undefined; + }); - ctrl.$parsers.push(minLengthValidator); - ctrl.$formatters.push(minLengthValidator); - } + ctrl.$formatters.push(function(value) { + if (value && !isDate(value)) { + throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value); + } + if (isValidDate(value)) { + previousDate = value; + if (previousDate && timezone === 'UTC') { + var timezoneOffset = 60000 * previousDate.getTimezoneOffset(); + previousDate = new Date(previousDate.getTime() + timezoneOffset); + } + return $filter('date')(value, format, timezone); + } else { + previousDate = null; + return ''; + } + }); - // max length validator - if (attr.ngMaxlength) { - var maxlength = int(attr.ngMaxlength); - var maxLengthValidator = function(value) { - return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value); - }; + if (isDefined(attr.min) || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + }; + attr.$observe('min', function(val) { + minVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } - ctrl.$parsers.push(maxLengthValidator); - ctrl.$formatters.push(maxLengthValidator); + if (isDefined(attr.max) || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + }; + attr.$observe('max', function(val) { + maxVal = parseObservedDateValue(val); + ctrl.$validate(); + }); + } + + function isValidDate(value) { + // Invalid Date: getTime() returns NaN + return value && !(value.getTime && value.getTime() !== value.getTime()); + } + + function parseObservedDateValue(val) { + return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined; + } + }; +} + +function badInputChecker(scope, element, attr, ctrl) { + var node = element[0]; + var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity); + if (nativeValidation) { + ctrl.$parsers.push(function(value) { + var validity = element.prop(VALIDITY_STATE_PROPERTY) || {}; + // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430): + // - also sets validity.badInput (should only be validity.typeMismatch). + // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email) + // - can ignore this case as we can still read out the erroneous email... + return validity.badInput && !validity.typeMismatch ? undefined : value; + }); } } function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); + badInputChecker(scope, element, attr, ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { - var empty = ctrl.$isEmpty(value); - if (empty || NUMBER_REGEXP.test(value)) { - ctrl.$setValidity('number', true); - return value === '' ? null : (empty ? value : parseFloat(value)); - } else { - ctrl.$setValidity('number', false); - return undefined; - } + if (ctrl.$isEmpty(value)) return null; + if (NUMBER_REGEXP.test(value)) return parseFloat(value); + return undefined; }); - addNativeHtml5Validators(ctrl, 'number', element); - ctrl.$formatters.push(function(value) { - return ctrl.$isEmpty(value) ? '' : '' + value; + if (!ctrl.$isEmpty(value)) { + if (!isNumber(value)) { + throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value); + } + value = value.toString(); + } + return value; }); - if (attr.min) { - var minValidator = function(value) { - var min = parseFloat(attr.min); - return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value); + if (attr.min || attr.ngMin) { + var minVal; + ctrl.$validators.min = function(value) { + return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; - ctrl.$parsers.push(minValidator); - ctrl.$formatters.push(minValidator); + attr.$observe('min', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + minVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - if (attr.max) { - var maxValidator = function(value) { - var max = parseFloat(attr.max); - return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value); + if (attr.max || attr.ngMax) { + var maxVal; + ctrl.$validators.max = function(value) { + return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; - ctrl.$parsers.push(maxValidator); - ctrl.$formatters.push(maxValidator); + attr.$observe('max', function(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val, 10); + } + maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); } - - ctrl.$formatters.push(function(value) { - return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value); - }); } function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var urlValidator = function(value) { - return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'url'; + ctrl.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); }; - - ctrl.$formatters.push(urlValidator); - ctrl.$parsers.push(urlValidator); } function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { - textInputType(scope, element, attr, ctrl, $sniffer, $browser); - - var emailValidator = function(value) { - return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value); + // Note: no badInputChecker here by purpose as `url` is only a validation + // in browsers, i.e. we can always read out input.value even if it is not valid! + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + stringBasedInputType(ctrl); + + ctrl.$$parserName = 'email'; + ctrl.$validators.email = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value); }; - - ctrl.$formatters.push(emailValidator); - ctrl.$parsers.push(emailValidator); } function radioInputType(scope, element, attr, ctrl) { @@ -16804,13 +19923,13 @@ function radioInputType(scope, element, attr, ctrl) { element.attr('name', nextUid()); } - element.on('click', function() { + var listener = function(ev) { if (element[0].checked) { - scope.$apply(function() { - ctrl.$setViewValue(attr.value); - }); + ctrl.$setViewValue(attr.value, ev && ev.type); } - }); + }; + + element.on('click', listener); ctrl.$render = function() { var value = attr.value; @@ -16820,30 +19939,42 @@ function radioInputType(scope, element, attr, ctrl) { attr.$observe('value', ctrl.$render); } -function checkboxInputType(scope, element, attr, ctrl) { - var trueValue = attr.ngTrueValue, - falseValue = attr.ngFalseValue; +function parseConstantExpr($parse, context, name, expression, fallback) { + var parseFn; + if (isDefined(expression)) { + parseFn = $parse(expression); + if (!parseFn.constant) { + throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' + + '`{1}`.', name, expression); + } + return parseFn(context); + } + return fallback; +} - if (!isString(trueValue)) trueValue = true; - if (!isString(falseValue)) falseValue = false; +function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true); + var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false); - element.on('click', function() { - scope.$apply(function() { - ctrl.$setViewValue(element[0].checked); - }); - }); + var listener = function(ev) { + ctrl.$setViewValue(element[0].checked, ev && ev.type); + }; + + element.on('click', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; }; - // Override the standard `$isEmpty` because a value of `false` means empty in a checkbox. + // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false` + // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert + // it to a boolean. ctrl.$isEmpty = function(value) { - return value !== trueValue; + return value === false; }; ctrl.$formatters.push(function(value) { - return value === trueValue; + return equals(value, trueValue); }); ctrl.$parsers.push(function(value) { @@ -16871,12 +20002,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * 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=} 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. */ @@ -16886,8 +20019,14 @@ function checkboxInputType(scope, element, attr, ctrl) { * @restrict E * * @description - * HTML input element control with angular data-binding. Input control follows HTML5 input types - * and polyfills the HTML5 validation behavior for older browsers. + * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding, + * input state control, and validation. + * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers. + * + * <div class="alert alert-warning"> + * **Note:** Not every feature offered is available for all input types. + * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`. + * </div> * * @param {string} ngModel Assignable angular expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. @@ -16896,22 +20035,27 @@ function checkboxInputType(scope, element, attr, ctrl) { * @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than * minlength. * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than - * maxlength. + * 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=} 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. + * This parameter is ignored for input[type=password] controls, which will never trim the + * input. * * @example - <example name="input-directive"> + <example name="input-directive" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.user = {name: 'guest', last: 'visitor'}; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = {name: 'guest', last: 'visitor'}; + }]); </script> - <div ng-controller="Ctrl"> + <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"> @@ -16936,7 +20080,7 @@ function checkboxInputType(scope, element, attr, ctrl) { </div> </file> <file name="protractor.js" type="protractor"> - var user = element(by.binding('{{user}}')); + var user = element(by.exactBinding('user')); var userNameValid = element(by.binding('myForm.userName.$valid')); var lastNameValid = element(by.binding('myForm.lastName.$valid')); var lastNameError = element(by.binding('myForm.lastName.$error')); @@ -16990,14 +20134,17 @@ function checkboxInputType(scope, element, attr, ctrl) { </file> </example> */ -var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { +var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', + function($browser, $sniffer, $filter, $parse) { return { restrict: 'E', - require: '?ngModel', - link: function(scope, element, attr, ctrl) { - if (ctrl) { - (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer, - $browser); + require: ['?ngModel'], + link: { + pre: function(scope, element, attr, ctrls) { + if (ctrls[0]) { + (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, + $browser, $filter, $parse); + } } } }; @@ -17006,55 +20153,117 @@ var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) { var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', - DIRTY_CLASS = 'ng-dirty'; + DIRTY_CLASS = 'ng-dirty', + UNTOUCHED_CLASS = 'ng-untouched', + TOUCHED_CLASS = 'ng-touched', + PENDING_CLASS = 'ng-pending'; /** * @ngdoc type * @name ngModel.NgModelController * * @property {string} $viewValue Actual string value in the view. - * @property {*} $modelValue The value in the model, that the control is bound to. + * @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. Each function is called, in turn, passing the value - through to the next. The last return value is used to populate the model. - Used to sanitize / convert the value as well as validation. For validation, - the parsers should update the validity state using - {@link ngModel.NgModelController#$setValidity $setValidity()}, - and return `undefined` for invalid values. + the control reads value from the DOM. The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. + +Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue +`$viewValue`}. + +Returning `undefined` from a parser means a parse error occurred. In that case, +no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` +will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} +is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. Each function is called, in turn, passing the value through to the - next. Used to format / convert values for display in the control and validation. - * ```js - * function formatter(value) { - * if (value) { - * return value.toUpperCase(); - * } - * } - * ngModel.$formatters.push(formatter); - * ``` + the model value changes. The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + Used to format / convert values for display in the control. + * ```js + * function formatter(value) { + * if (value) { + * return value.toUpperCase(); + * } + * } + * ngModel.$formatters.push(formatter); + * ``` + * + * @property {Object.<string, function>} $validators A collection of validators that are applied + * whenever the model value changes. The key value within the object refers to the name of the + * validator while the function refers to the validation operation. The validation operation is + * provided with the model value as an argument and must return a true or false value depending + * on the response of that validation. + * + * ```js + * ngModel.$validators.validCharacters = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * return /[0-9]+/.test(value) && + * /[a-z]+/.test(value) && + * /[A-Z]+/.test(value) && + * /\W+/.test(value); + * }; + * ``` + * + * @property {Object.<string, function>} $asyncValidators A collection of validations that are expected to + * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided + * is expected to return a promise when it is run during the model validation process. Once the promise + * is delivered then the validation status will be set to true when fulfilled and false when rejected. + * When the asynchronous validators are triggered, each of the validators will run in parallel and the model + * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator + * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators + * will only run once all synchronous validators have passed. + * + * Please note that if $http is used then it is important that the server returns a success HTTP response code + * in order to fulfill the validation and a status level of `4xx` in order to reject the validation. + * + * ```js + * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { + * var value = modelValue || viewValue; + * + * // Lookup user by username + * return $http.get('/api/users/' + value). + * then(function resolved() { + * //username exists, this means validation fails + * return $q.reject('exists'); + * }, function rejected() { + * //username does not exist, therefore this validation passes + * return true; + * }); + * }; + * ``` * * @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the * view value has changed. It is called with no arguments, and its return value is ignored. * This can be used in place of additional $watches against the model value. * - * @property {Object} $error An object hash with all errors as keys. + * @property {Object} $error An object hash with all failing validator ids as keys. + * @property {Object} $pending An object hash with all pending validator ids as keys. * + * @property {boolean} $untouched True if control has not lost focus yet. + * @property {boolean} $touched True if control has lost focus. * @property {boolean} $pristine True if user has not interacted with the control yet. * @property {boolean} $dirty True if user has already interacted with the control. * @property {boolean} $valid True if there is no error. * @property {boolean} $invalid True if at least one error on the control. + * @property {string} $name The name attribute of the control. * * @description * - * `NgModelController` provides API for the `ng-model` directive. The controller contains - * services for data-binding, validation, CSS updates, and value formatting and parsing. It - * purposefully does not contain any logic which deals with DOM rendering or listening to - * DOM events. Such DOM related logic should be provided by other directives which make use of - * `NgModelController` for data-binding. + * `NgModelController` provides API for the {@link ngModel `ngModel`} directive. + * The controller contains services for data-binding, validation, CSS updates, and value formatting + * and parsing. It purposefully does not contain any logic which deals with DOM rendering or + * listening to DOM events. + * Such DOM related logic should be provided by other directives which make use of + * `NgModelController` for data-binding to control elements. + * Angular provides this DOM logic for most {@link input `input`} elements. + * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example + * custom control example} that uses `ngModelController` to bind to `contenteditable` elements. * - * ## Custom Control Example + * @example + * ### Custom Control Example * This example shows how to use `NgModelController` with a custom control to achieve * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) * collaborate together to achieve the desired result. @@ -17062,7 +20271,12 @@ var VALID_CLASS = 'ng-valid', * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element * contents be edited in place by the user. This will not work on older browsers. * - * <example name="NgModelController" module="customControl"> + * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize} + * module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`). + * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks + * that content using the `$sce` service. + * + * <example name="NgModelController" module="customControl" deps="angular-sanitize.js"> <file name="style.css"> [contenteditable] { border: 1px solid black; @@ -17076,22 +20290,22 @@ var VALID_CLASS = 'ng-valid', </file> <file name="script.js"> - angular.module('customControl', []). - directive('contenteditable', function() { + angular.module('customControl', ['ngSanitize']). + directive('contenteditable', ['$sce', function($sce) { return { restrict: 'A', // only activate on element attribute require: '?ngModel', // get a hold of NgModelController link: function(scope, element, attrs, ngModel) { - if(!ngModel) return; // do nothing if no ng-model + if (!ngModel) return; // do nothing if no ng-model // Specify how UI should be updated ngModel.$render = function() { - element.html(ngModel.$viewValue || ''); + element.html($sce.getTrustedHtml(ngModel.$viewValue || '')); }; // Listen for change events to enable binding element.on('blur keyup change', function() { - scope.$apply(read); + scope.$evalAsync(read); }); read(); // initialize @@ -17100,14 +20314,14 @@ var VALID_CLASS = 'ng-valid', var html = element.html(); // When we clear the content editable the browser leaves a <br> behind // If strip-br attribute is provided then we strip this out - if( attrs.stripBr && html == '<br>' ) { + if ( attrs.stripBr && html == '<br>' ) { html = ''; } ngModel.$setViewValue(html); } } }; - }); + }]); </file> <file name="index.html"> <form name="myForm"> @@ -17142,26 +20356,60 @@ var VALID_CLASS = 'ng-valid', * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate) { +var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', + function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; + this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. + this.$validators = {}; + this.$asyncValidators = {}; this.$parsers = []; this.$formatters = []; this.$viewChangeListeners = []; + this.$untouched = true; + this.$touched = false; this.$pristine = true; this.$dirty = false; this.$valid = true; this.$invalid = false; - this.$name = $attr.name; - - var ngModelGet = $parse($attr.ngModel), - ngModelSet = ngModelGet.assign; - - if (!ngModelSet) { - throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); - } + this.$error = {}; // keep invalid keys here + this.$$success = {}; // keep valid keys here + this.$pending = undefined; // keep pending keys here + this.$name = $interpolate($attr.name || '', false)($scope); + + + var parsedNgModel = $parse($attr.ngModel), + parsedNgModelAssign = parsedNgModel.assign, + ngModelGet = parsedNgModel, + ngModelSet = parsedNgModelAssign, + pendingDebounce = null, + ctrl = this; + + this.$$setOptions = function(options) { + ctrl.$options = options; + if (options && options.getterSetter) { + var invokeModelGetter = $parse($attr.ngModel + '()'), + invokeModelSetter = $parse($attr.ngModel + '($$$p)'); + + ngModelGet = function($scope) { + var modelValue = parsedNgModel($scope); + if (isFunction(modelValue)) { + modelValue = invokeModelGetter($scope); + } + return modelValue; + }; + ngModelSet = function($scope, newValue) { + if (isFunction(parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + } else { + parsedNgModelAssign($scope, ctrl.$modelValue); + } + }; + } else if (!parsedNgModel.assign) { + throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", + $attr.ngModel, startingTag($element)); + } + }; /** * @ngdoc method @@ -17170,6 +20418,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Called when the view needs to be updated. It is expected that the user of the ng-model * directive will implement this method. + * + * The `$render()` method is invoked in the following situations: + * + * * `$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. + * + * 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` + * 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. */ this.$render = noop; @@ -17178,96 +20438,427 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @name ngModel.NgModelController#$isEmpty * * @description - * This is called when we need to determine if the value of the input is empty. + * This is called when we need to determine if the value of an input is empty. * * For instance, the required directive does this to work out if the input has data or not. + * * 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 * default. The `checkboxInputType` directive does this because in its case a value of `false` * implies empty. * - * @param {*} value Reference to check. - * @returns {boolean} True if `value` is empty. + * @param {*} value The value of the input to check for emptiness. + * @returns {boolean} True if `value` is "empty". */ this.$isEmpty = function(value) { return isUndefined(value) || value === '' || value === null || value !== value; }; var parentForm = $element.inheritedData('$formController') || nullFormCtrl, - invalidCount = 0, // used to easily determine if we are valid - $error = this.$error = {}; // keep invalid keys here - - - // Setup initial state of the control - $element.addClass(PRISTINE_CLASS); - toggleValidCss(true); - - // convenience method for easy toggling of classes - function toggleValidCss(isValid, validationErrorKey) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; - $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey); - $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey); - } + currentValidationRunId = 0; /** * @ngdoc method * @name ngModel.NgModelController#$setValidity * * @description - * Change the validity state, and notifies the form when the control changes validity. (i.e. it - * does not notify form if given validator is already marked as invalid). + * Change the validity state, and notify the form. * - * This method should be called by validators - i.e. the parser or formatter functions. + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. * - * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign - * to `$error[validationErrorKey]=isValid` so that it is available for data-binding. + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. * The `validationErrorKey` should be in camelCase and will get converted into dash-case * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true) or invalid (false). + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ + addSetValidityMethod({ + ctrl: this, + $element: $element, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + }, + parentForm: parentForm, + $animate: $animate + }); + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setPristine + * + * @description + * Sets the control to its pristine state. + * + * This method can be called to remove the `ng-dirty` class and set the control to its pristine + * state (`ng-pristine` class). A model is considered to be pristine when the control + * has not been changed from when first compiled. + */ + this.$setPristine = function() { + ctrl.$dirty = false; + ctrl.$pristine = true; + $animate.removeClass($element, DIRTY_CLASS); + $animate.addClass($element, PRISTINE_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setDirty + * + * @description + * Sets the control to its dirty state. + * + * This method can be called to remove the `ng-pristine` class and set the control to its dirty + * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed + * from when first compiled. + */ + this.$setDirty = function() { + ctrl.$dirty = true; + ctrl.$pristine = false; + $animate.removeClass($element, PRISTINE_CLASS); + $animate.addClass($element, DIRTY_CLASS); + parentForm.$setDirty(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setUntouched + * + * @description + * Sets the control to its untouched state. + * + * This method can be called to remove the `ng-touched` class and set the control to its + * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched + * by default, however this function can be used to restore that state if the model has + * already been touched by the user. + */ + this.$setUntouched = function() { + ctrl.$touched = false; + ctrl.$untouched = true; + $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$setTouched + * + * @description + * Sets the control to its touched state. + * + * This method can be called to remove the `ng-untouched` class and set the control to its + * touched state (`ng-touched` class). A model is considered to be touched when the user has + * first focused the control element and then shifted focus away from the control (blur event). + */ + this.$setTouched = function() { + ctrl.$touched = true; + ctrl.$untouched = false; + $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$rollbackViewValue + * + * @description + * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, + * 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`. + * + * In this case, you can 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. + * + * The `$rollbackViewValue()` method should be called before programmatically changing the model of an + * input which may have such events pending. This is important in order to make sure that the + * input field will be updated with the new model value and any pending operations are cancelled. + * + * <example name="ng-model-cancel-update" module="cancel-update-example"> + * <file name="app.js"> + * 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) { + * if (e.keyCode == 27) { + * $scope.myValue = ''; + * } + * }; + * }]); + * </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> + * + * <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 }}" + * </form> + * </div> + * </file> + * </example> + */ + this.$rollbackViewValue = function() { + $timeout.cancel(pendingDebounce); + ctrl.$viewValue = ctrl.$$lastCommittedViewValue; + ctrl.$render(); + }; + + /** + * @ngdoc method + * @name ngModel.NgModelController#$validate + * + * @description + * Runs each of the registered validators (first synchronous validators and then + * asynchronous validators). + * If the validity changes to invalid, the model will be set to `undefined`, + * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`. + * If the validity changes to valid, it will set the model to the last available valid + * modelValue, i.e. either the last parsed value or the last value set from the scope. */ - this.$setValidity = function(validationErrorKey, isValid) { - // Purposeful use of ! here to cast isValid to boolean in case it is undefined - // jshint -W018 - if ($error[validationErrorKey] === !isValid) return; - // jshint +W018 - - if (isValid) { - if ($error[validationErrorKey]) invalidCount--; - if (!invalidCount) { - toggleValidCss(true); - this.$valid = true; - this.$invalid = false; + this.$validate = function() { + // ignore $validate before model is initialized + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + return; + } + + var viewValue = ctrl.$$lastCommittedViewValue; + // Note: we use the $$rawModelValue as $modelValue might have been + // set to undefined during a view -> model update that found validation + // errors. We can't parse the view here, since that could change + // the model although neither viewValue nor the model on the scope changed + var modelValue = ctrl.$$rawModelValue; + + // Check if the there's a parse error, so we don't unset it accidentially + var parserName = ctrl.$$parserName || 'parse'; + var parserValid = ctrl.$error[parserName] ? false : undefined; + + var prevValid = ctrl.$valid; + var prevModelValue = ctrl.$modelValue; + + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + + ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) { + // If there was no change in validity, don't update the model + // This prevents changing an invalid modelValue to undefined + if (!allowInvalid && prevValid !== allValid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } } - } else { - toggleValidCss(false); - this.$invalid = true; - this.$valid = false; - invalidCount++; + }); + + }; + + this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) { + currentValidationRunId++; + var localValidationRunId = currentValidationRunId; + + // check parser error + if (!processParseErrors(parseValid)) { + validationDone(false); + return; } + if (!processSyncValidators()) { + validationDone(false); + return; + } + processAsyncValidators(); - $error[validationErrorKey] = !isValid; - toggleValidCss(isValid, validationErrorKey); + function processParseErrors(parseValid) { + var errorKey = ctrl.$$parserName || 'parse'; + if (parseValid === undefined) { + setValidity(errorKey, null); + } else { + setValidity(errorKey, parseValid); + if (!parseValid) { + forEach(ctrl.$validators, function(v, name) { + setValidity(name, null); + }); + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + } + return true; + } - parentForm.$setValidity(validationErrorKey, isValid, this); + function processSyncValidators() { + var syncValidatorsValid = true; + forEach(ctrl.$validators, function(validator, name) { + var result = validator(modelValue, viewValue); + syncValidatorsValid = syncValidatorsValid && result; + setValidity(name, result); + }); + if (!syncValidatorsValid) { + forEach(ctrl.$asyncValidators, function(v, name) { + setValidity(name, null); + }); + return false; + } + return true; + } + + function processAsyncValidators() { + var validatorPromises = []; + var allValid = true; + forEach(ctrl.$asyncValidators, function(validator, name) { + var promise = validator(modelValue, viewValue); + if (!isPromiseLike(promise)) { + throw $ngModelMinErr("$asyncValidators", + "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + } + setValidity(name, undefined); + validatorPromises.push(promise.then(function() { + setValidity(name, true); + }, function(error) { + allValid = false; + setValidity(name, false); + })); + }); + if (!validatorPromises.length) { + validationDone(true); + } else { + $q.all(validatorPromises).then(function() { + validationDone(allValid); + }, noop); + } + } + + function setValidity(name, isValid) { + if (localValidationRunId === currentValidationRunId) { + ctrl.$setValidity(name, isValid); + } + } + + function validationDone(allValid) { + if (localValidationRunId === currentValidationRunId) { + + doneCallback(allValid); + } + } }; /** * @ngdoc method - * @name ngModel.NgModelController#$setPristine + * @name ngModel.NgModelController#$commitViewValue * * @description - * Sets the control to its pristine state. + * Commit a pending update to the `$modelValue`. * - * This method can be called to remove the 'ng-dirty' class and set the control to its pristine - * state (ng-pristine class). + * Updates may be pending by a debounced event or because the input is waiting for a some future + * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` + * usually handles calling this in response to input events. */ - this.$setPristine = function () { - this.$dirty = false; - this.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); + this.$commitViewValue = function() { + var viewValue = ctrl.$viewValue; + + $timeout.cancel(pendingDebounce); + + // If the view value has not changed then we should just exit, except in the case where there is + // a native validator on the element. In this case the validation state may have changed even though + // the viewValue has stayed empty. + if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + return; + } + ctrl.$$lastCommittedViewValue = viewValue; + + // change to dirty + if (ctrl.$pristine) { + this.$setDirty(); + } + this.$$parseAndValidate(); + }; + + this.$$parseAndValidate = function() { + var viewValue = ctrl.$$lastCommittedViewValue; + var modelValue = viewValue; + var parserValid = isUndefined(modelValue) ? undefined : true; + + if (parserValid) { + for (var i = 0; i < ctrl.$parsers.length; i++) { + modelValue = ctrl.$parsers[i](modelValue); + if (isUndefined(modelValue)) { + parserValid = false; + break; + } + } + } + if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + // ctrl.$modelValue has not been touched yet... + ctrl.$modelValue = ngModelGet($scope); + } + var prevModelValue = ctrl.$modelValue; + var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + ctrl.$$rawModelValue = modelValue; + + if (allowInvalid) { + ctrl.$modelValue = modelValue; + writeToModelIfNeeded(); + } + + // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. + // This can happen if e.g. $setViewValue is called from inside a parser + ctrl.$$runValidators(parserValid, modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + if (!allowInvalid) { + // Note: Don't check ctrl.$valid here, as we could have + // external validators (e.g. calculated on the server), + // that just call $setValidity and need the model value + // to calculate their validity. + ctrl.$modelValue = allValid ? modelValue : undefined; + writeToModelIfNeeded(); + } + }); + + function writeToModelIfNeeded() { + if (ctrl.$modelValue !== prevModelValue) { + ctrl.$$writeModelToScope(); + } + } + }; + + this.$$writeModelToScope = function() { + ngModelSet($scope, ctrl.$modelValue); + forEach(ctrl.$viewChangeListeners, function(listener) { + try { + listener(); + } catch (e) { + $exceptionHandler(e); + } + }); }; /** @@ -17277,73 +20868,108 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @description * Update the view value. * - * This method should be called when the view value changes, typically from within a DOM event handler. - * For example {@link ng.directive:input input} and - * {@link ng.directive:select select} directives call it. + * 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. * - * It will update the $viewValue, then pass this value through each of the functions in `$parsers`, - * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to - * `$modelValue` and the **expression** specified in the `ng-model` attribute. + * 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` + * 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. * + * 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 calling this function does not trigger a `$digest`. * * @param {string} value Value from the view. + * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value) { - this.$viewValue = value; - - // change to dirty - if (this.$pristine) { - this.$dirty = true; - this.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - parentForm.$setDirty(); + this.$setViewValue = function(value, trigger) { + ctrl.$viewValue = value; + if (!ctrl.$options || ctrl.$options.updateOnDefault) { + ctrl.$$debounceViewValueCommit(trigger); } + }; - forEach(this.$parsers, function(fn) { - value = fn(value); - }); + this.$$debounceViewValueCommit = function(trigger) { + var debounceDelay = 0, + options = ctrl.$options, + debounce; + + if (options && isDefined(options.debounce)) { + debounce = options.debounce; + if (isNumber(debounce)) { + debounceDelay = debounce; + } else if (isNumber(debounce[trigger])) { + debounceDelay = debounce[trigger]; + } else if (isNumber(debounce['default'])) { + debounceDelay = debounce['default']; + } + } - if (this.$modelValue !== value) { - this.$modelValue = value; - ngModelSet($scope, value); - forEach(this.$viewChangeListeners, function(listener) { - try { - listener(); - } catch(e) { - $exceptionHandler(e); - } + $timeout.cancel(pendingDebounce); + if (debounceDelay) { + pendingDebounce = $timeout(function() { + ctrl.$commitViewValue(); + }, debounceDelay); + } else if ($rootScope.$$phase) { + ctrl.$commitViewValue(); + } else { + $scope.$apply(function() { + ctrl.$commitViewValue(); }); } }; // model -> value - var ctrl = this; - + // Note: we cannot use a normal scope.$watch as we want to detect the following: + // 1. scope value is 'a' + // 2. user enters 'b' + // 3. ng-change kicks in and reverts scope value to 'a' + // -> scope value did not change since the last digest as + // ng-change executes in apply phase + // 4. view should be changed back to 'a' $scope.$watch(function ngModelWatch() { - var value = ngModelGet($scope); + var modelValue = ngModelGet($scope); // if scope model value and ngModel value are out of sync - if (ctrl.$modelValue !== value) { + // TODO(perf): why not move this to the action fn? + if (modelValue !== ctrl.$modelValue) { + ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; var formatters = ctrl.$formatters, idx = formatters.length; - ctrl.$modelValue = value; - while(idx--) { - value = formatters[idx](value); + var viewValue = modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); } - - if (ctrl.$viewValue !== value) { - ctrl.$viewValue = value; + if (ctrl.$viewValue !== viewValue) { + ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); + + ctrl.$$runValidators(undefined, modelValue, viewValue, noop); } } - return value; + return modelValue; }); }]; @@ -17353,6 +20979,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @name ngModel * * @element input + * @priority 1 * * @description * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a @@ -17364,8 +20991,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - Binding the view into the model, which other directives such as `input`, `textarea` or `select` * require. * - Providing validation behavior (i.e. required, number, email, url). - * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors). - * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations. + * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors). + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations. * - Registering the control with its parent {@link ng.directive:form form}. * * Note: `ngModel` will try to bind to the property given by evaluating the expression on the @@ -17374,7 +21001,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * For best practices on using `ngModel`, see: * - * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes] + * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes) * * For basic examples, how to use `ngModel`, see: * @@ -17385,6 +21012,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link input[number] number} * - {@link input[email] email} * - {@link input[url] url} + * - {@link input[date] date} + * - {@link input[datetime-local] datetime-local} + * - {@link input[time] time} + * - {@link input[month] month} + * - {@link input[week] week} * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * @@ -17392,10 +21024,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * The following CSS classes are added and removed on the associated input/select/textarea element * depending on the validity of the model. * - * - `ng-valid` is set if the model is valid. - * - `ng-invalid` is set if the model is invalid. - * - `ng-pristine` is set if the model is pristine. - * - `ng-dirty` is set if the model is dirty. + * - `ng-valid`: the model is valid + * - `ng-invalid`: the model is invalid + * - `ng-valid-[key]`: for each valid key added by `$setValidity` + * - `ng-invalid-[key]`: for each invalid key added by `$setValidity` + * - `ng-pristine`: the control hasn't been interacted with yet + * - `ng-dirty`: the control has been interacted with + * - `ng-touched`: the control has been blurred + * - `ng-untouched`: the control hasn't been blurred + * - `ng-pending`: any `$asyncValidators` are unfulfilled * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * @@ -17424,12 +21061,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * </pre> * * @example - * <example deps="angular-animate.js" animations="true" fixBase="true"> + * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.val = '1'; - } + angular.module('inputExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.val = '1'; + }]); </script> <style> .my-input { @@ -17444,30 +21082,116 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ </style> Update input to see transitions when valid/invalid. Integer is a valid value. - <form name="testForm" ng-controller="Ctrl"> + <form name="testForm" ng-controller="ExampleController"> <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" /> </form> </file> * </example> + * + * ## Binding to a getter/setter + * + * 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 + * to the view. + * + * <div class="alert alert-success"> + * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more + * frequently than other parts of your code. + * </div> + * + * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that + * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to + * a `<form>`, which will enable this behavior for all `<input>`s within it. See + * {@link ng.directive:ngModelOptions `ngModelOptions`} for more. + * + * The following example shows how to use `ngModel` with a getter/setter: + * + * @example + * <example name="ngModel-getter-setter" module="getterSetterExample"> + <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 }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + if (angular.isDefined(newName)) { + _name = newName; + } + return _name; + } + }; + }]); + </file> + * </example> */ -var ngModelDirective = function() { +var ngModelDirective = ['$rootScope', function($rootScope) { return { - require: ['ngModel', '^?form'], + restrict: 'A', + require: ['ngModel', '^?form', '^?ngModelOptions'], controller: NgModelController, - link: function(scope, element, attr, ctrls) { - // notify others, especially parent forms + // Prelink needs to run before any input directive + // so that we can set the NgModelOptions in NgModelController + // before anyone else uses it. + priority: 1, + compile: function ngModelCompile(element) { + // Setup initial state of the control + element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS); - var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || nullFormCtrl; + return { + pre: function ngModelPreLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0], + formCtrl = ctrls[1] || nullFormCtrl; - formCtrl.$addControl(modelCtrl); + modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); - scope.$on('$destroy', function() { - formCtrl.$removeControl(modelCtrl); - }); + // notify others, especially parent forms + formCtrl.$addControl(modelCtrl); + + attr.$observe('name', function(newValue) { + if (modelCtrl.$name !== newValue) { + formCtrl.$$renameControl(modelCtrl, newValue); + } + }); + + scope.$on('$destroy', function() { + formCtrl.$removeControl(modelCtrl); + }); + }, + post: function ngModelPostLink(scope, element, attr, ctrls) { + var modelCtrl = ctrls[0]; + if (modelCtrl.$options && modelCtrl.$options.updateOn) { + element.on(modelCtrl.$options.updateOn, function(ev) { + modelCtrl.$$debounceViewValueCommit(ev && ev.type); + }); + } + + element.on('blur', function(ev) { + if (modelCtrl.$touched) return; + + if ($rootScope.$$phase) { + scope.$evalAsync(modelCtrl.$setTouched); + } else { + scope.$apply(modelCtrl.$setTouched); + } + }); + } + }; } }; -}; +}]; /** @@ -17479,7 +21203,15 @@ var ngModelDirective = function() { * The expression is evaluated immediately, unlike the JavaScript onchange event * which only triggers at the end of a change (usually, when the user leaves the * form element or presses the return key). - * The expression is not evaluated when the value change is coming from the model. + * + * The `ngChange` expression is only evaluated when a change in the input value causes + * a new value to be committed to the model. + * + * It will not be evaluated: + * * if the value returned from the `$parsers` transformation pipeline has not changed + * * if the input has continued to be invalid since the model will stay `null` + * * if the model is changed programmatically and not by a change to the input value + * * * Note, this directive requires `ngModel` to be present. * @@ -17488,17 +21220,18 @@ var ngModelDirective = function() { * in input value. * * @example - * <example name="ngChange-directive"> + * <example name="ngChange-directive" module="changeExample"> * <file name="index.html"> * <script> - * function Controller($scope) { - * $scope.counter = 0; - * $scope.change = function() { - * $scope.counter++; - * }; - * } + * angular.module('changeExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.counter = 0; + * $scope.change = function() { + * $scope.counter++; + * }; + * }]); * </script> - * <div ng-controller="Controller"> + * <div ng-controller="ExampleController"> * <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" /> * <input type="checkbox" ng-model="confirmed" id="ng-change-example2" /> * <label for="ng-change-example2">Confirmed</label><br /> @@ -17529,6 +21262,7 @@ var ngModelDirective = function() { * </example> */ var ngChangeDirective = valueFn({ + restrict: 'A', require: 'ngModel', link: function(scope, element, attr, ctrl) { ctrl.$viewChangeListeners.push(function() { @@ -17540,27 +21274,90 @@ var ngChangeDirective = valueFn({ var requiredDirective = function() { return { + restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.required = true; // force truthy in case we are on non input element - var validator = function(value) { - if (attr.required && ctrl.$isEmpty(value)) { - ctrl.$setValidity('required', false); - return; - } else { - ctrl.$setValidity('required', true); - return value; + ctrl.$validators.required = function(modelValue, viewValue) { + return !attr.required || !ctrl.$isEmpty(viewValue); + }; + + attr.$observe('required', function() { + ctrl.$validate(); + }); + } + }; +}; + + +var patternDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var regexp, patternExp = attr.ngPattern || attr.pattern; + attr.$observe('pattern', function(regex) { + if (isString(regex) && regex.length > 0) { + regex = new RegExp('^' + regex + '$'); + } + + if (regex && !regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(elm)); } + + regexp = regex || undefined; + ctrl.$validate(); + }); + + ctrl.$validators.pattern = function(value) { + return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; + } + }; +}; - ctrl.$formatters.push(validator); - ctrl.$parsers.unshift(validator); - attr.$observe('required', function() { - validator(ctrl.$viewValue); +var maxlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var maxlength = -1; + attr.$observe('maxlength', function(value) { + var intVal = int(value); + maxlength = isNaN(intVal) ? -1 : intVal; + ctrl.$validate(); }); + ctrl.$validators.maxlength = function(modelValue, viewValue) { + return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (viewValue.length <= maxlength); + }; + } + }; +}; + +var minlengthDirective = function() { + return { + restrict: 'A', + require: '?ngModel', + link: function(scope, elm, attr, ctrl) { + if (!ctrl) return; + + var minlength = 0; + attr.$observe('minlength', function(value) { + minlength = int(value) || 0; + ctrl.$validate(); + }); + ctrl.$validators.minlength = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; + }; } }; }; @@ -17571,61 +21368,94 @@ var requiredDirective = function() { * @name ngList * * @description - * Text input that converts between a delimited string and an array of strings. The delimiter - * can be a fixed string (by default a comma) or a regular expression. + * Text input that converts between a delimited string and an array of strings. The default + * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom + * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`. + * + * The behaviour of the directive is affected by the use of the `ngTrim` attribute. + * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each + * list item is respected. This implies that the user of the directive is responsible for + * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a + * tab or newline character. + * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected + * when joining the list items back together) and whitespace around each list item is stripped + * before it is added to the model. + * + * ### Example with Validation + * + * <example name="ngList-directive" module="listExample"> + * <file name="app.js"> + * angular.module('listExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.names = ['morpheus', 'neo', 'trinity']; + * }]); + * </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"> + * Required!</span> + * <br> + * <tt>names = {{names}}</tt><br/> + * <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> + * <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> + * <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> + * <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + * </form> + * </file> + * <file name="protractor.js" type="protractor"> + * var listInput = element(by.model('names')); + * var names = element(by.exactBinding('names')); + * var valid = element(by.binding('myForm.namesInput.$valid')); + * var error = element(by.css('span.error')); + * + * it('should initialize to model', function() { + * expect(names.getText()).toContain('["morpheus","neo","trinity"]'); + * expect(valid.getText()).toContain('true'); + * expect(error.getCssValue('display')).toBe('none'); + * }); * - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. If - * specified in form `/something/` then the value will be converted into a regular expression. + * it('should be invalid if empty', function() { + * listInput.clear(); + * listInput.sendKeys(''); * - * @example - <example name="ngList-directive"> - <file name="index.html"> - <script> - function Ctrl($scope) { - $scope.names = ['igor', 'misko', 'vojta']; - } - </script> - <form name="myForm" ng-controller="Ctrl"> - List: <input name="namesInput" ng-model="names" ng-list required> - <span class="error" ng-show="myForm.namesInput.$error.required"> - Required!</span> - <br> - <tt>names = {{names}}</tt><br/> - <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/> - <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> - </form> - </file> - <file name="protractor.js" type="protractor"> - var listInput = element(by.model('names')); - var names = element(by.binding('{{names}}')); - var valid = element(by.binding('myForm.namesInput.$valid')); - var error = element(by.css('span.error')); - - it('should initialize to model', function() { - expect(names.getText()).toContain('["igor","misko","vojta"]'); - expect(valid.getText()).toContain('true'); - expect(error.getCssValue('display')).toBe('none'); - }); - - it('should be invalid if empty', function() { - listInput.clear(); - listInput.sendKeys(''); - - expect(names.getText()).toContain(''); - expect(valid.getText()).toContain('false'); - expect(error.getCssValue('display')).not.toBe('none'); }); - </file> - </example> + * expect(names.getText()).toContain(''); + * expect(valid.getText()).toContain('false'); + * expect(error.getCssValue('display')).not.toBe('none'); + * }); + * </file> + * </example> + * + * ### Example - splitting on whitespace + * <example name="ngList-directive-newlines"> + * <file name="index.html"> + * <textarea ng-model="list" ng-list=" " ng-trim="false"></textarea> + * <pre>{{ list | json }}</pre> + * </file> + * <file name="protractor.js" type="protractor"> + * it("should split the text by newlines", function() { + * var listInput = element(by.model('list')); + * var output = element(by.binding('list | json')); + * listInput.sendKeys('abc\ndef\nghi'); + * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]'); + * }); + * </file> + * </example> + * + * @element input + * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { + restrict: 'A', + priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - var match = /\/(.*)\//.exec(attr.ngList), - separator = match && new RegExp(match[1]) || attr.ngList || ','; + // We want to control whitespace trimming so we use this convoluted approach + // to access the ngList attribute, which doesn't pre-trim the attribute + var ngList = element.attr(attr.$attr.ngList) || ', '; + var trimValues = attr.ngTrim !== 'false'; + var separator = trimValues ? trim(ngList) : ngList; var parse = function(viewValue) { // If the viewValue is invalid (say required but empty) it will be `undefined` @@ -17635,7 +21465,7 @@ var ngListDirective = function() { if (viewValue) { forEach(viewValue.split(separator), function(value) { - if (value) list.push(trim(value)); + if (value) list.push(trimValues ? trim(value) : value); }); } @@ -17645,7 +21475,7 @@ var ngListDirective = function() { ctrl.$parsers.push(parse); ctrl.$formatters.push(function(value) { if (isArray(value)) { - return value.join(', '); + return value.join(ngList); } return undefined; @@ -17666,27 +21496,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `input[select]` or `input[radio]`, so - * that when the element is selected, the `ngModel` of that element is set to the - * bound value. + * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`}, + * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to + * the bound value. * - * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as - * shown below. + * `ngValue` is useful when dynamically generating lists of radio buttons using + * {@link ngRepeat `ngRepeat`}, as shown below. + * + * Likewise, `ngValue` can be used to generate `<option>` elements for + * the {@link select `select`} element. In that case however, only strings are supported + * for the `value `attribute, so the resulting `ngModel` will always be a string. + * Support for `select` models with non-string values is available via `ngOptions`. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute * of the `input` element * * @example - <example name="ngValue-directive"> + <example name="ngValue-directive" module="valueExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.names = ['pizza', 'unicorns', 'robots']; - $scope.my = { favorite: 'unicorns' }; - } + angular.module('valueExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.names = ['pizza', 'unicorns', 'robots']; + $scope.my = { favorite: 'unicorns' }; + }]); </script> - <form ng-controller="Ctrl"> + <form ng-controller="ExampleController"> <h2>Which is your favorite?</h2> <label ng-repeat="name in names" for="{{name}}"> {{name}} @@ -17714,6 +21550,7 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; */ var ngValueDirective = function() { return { + restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { @@ -17733,6 +21570,281 @@ var ngValueDirective = function() { /** * @ngdoc directive + * @name ngModelOptions + * + * @description + * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of + * events that will trigger a model update and/or a debouncing delay so that the actual update only + * 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 + * 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. + * + * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} + * method is by making sure the input is placed inside a form that has a `name` attribute. This is + * important because `form` controllers are published to the related scope under the name in their + * `name` attribute. + * + * Any pending changes will take place immediately when an enclosing form is submitted via the + * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` + * to have access to the updated model. + * + * `ngModelOptions` has an effect on the element it's declared on and its descendants. + * + * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * - `updateOn`: string specifying which event should the input be bound to. You can set several + * events using an space delimited list. There is a special event called `default` that + * matches the default events belonging of the control. + * - `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} }"` + * - `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. + * + * @example + + The following example shows how to override immediate updates. Changes on the inputs within the + form will update the model only when the control loses focus (blur event). If `escape` key is + pressed while the input field is focused, the value is reset to the value in the current model. + + <example name="ngModelOptions-directive-blur" module="optionsExample"> + <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 /> + </form> + <pre>user.name = <span ng-bind="user.name"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say', data: '' }; + + $scope.cancel = function(e) { + if (e.keyCode == 27) { + $scope.userForm.userName.$rollbackViewValue(); + } + }; + }]); + </file> + <file name="protractor.js" type="protractor"> + var model = element(by.binding('user.name')); + var input = element(by.model('user.name')); + var other = element(by.model('user.data')); + + it('should allow custom events', function() { + input.sendKeys(' hello'); + input.click(); + expect(model.getText()).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say hello'); + }); + + it('should $rollbackViewValue when model changes', function() { + input.sendKeys(' hello'); + expect(input.getAttribute('value')).toEqual('say hello'); + input.sendKeys(protractor.Key.ESCAPE); + expect(input.getAttribute('value')).toEqual('say'); + other.click(); + expect(model.getText()).toEqual('say'); + }); + </file> + </example> + + This one shows how to debounce model changes. Model will be updated only 1 sec after last change. + If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + + <example name="ngModelOptions-directive-debounce" module="optionsExample"> + <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 /> + </form> + <pre>user.name = <span ng-bind="user.name"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('optionsExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.user = { name: 'say' }; + }]); + </file> + </example> + + This one shows how to bind to getter/setters: + + <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> + <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 }" /> + </form> + <pre>user.name = <span ng-bind="user.name()"></span></pre> + </div> + </file> + <file name="app.js"> + angular.module('getterSetterExample', []) + .controller('ExampleController', ['$scope', function($scope) { + var _name = 'Brian'; + $scope.user = { + name: function(newName) { + return angular.isDefined(newName) ? (_name = newName) : _name; + } + }; + }]); + </file> + </example> + */ +var ngModelOptionsDirective = function() { + return { + restrict: 'A', + controller: ['$scope', '$attrs', function($scope, $attrs) { + var that = this; + this.$options = $scope.$eval($attrs.ngModelOptions); + // Allow adding/overriding bound events + if (this.$options.updateOn !== undefined) { + 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() { + that.$options.updateOnDefault = true; + return ' '; + })); + } else { + this.$options.updateOnDefault = true; + } + }] + }; +}; + +// helper methods +function addSetValidityMethod(context) { + var ctrl = context.ctrl, + $element = context.$element, + classCache = {}, + set = context.set, + unset = context.unset, + parentForm = context.parentForm, + $animate = context.$animate; + + classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); + + ctrl.$setValidity = setValidity; + + function setValidity(validationErrorKey, state, options) { + if (state === undefined) { + createAndSet('$pending', validationErrorKey, options); + } else { + unsetAndCleanup('$pending', validationErrorKey, options); + } + if (!isBoolean(state)) { + unset(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } else { + if (state) { + unset(ctrl.$error, validationErrorKey, options); + set(ctrl.$$success, validationErrorKey, options); + } else { + set(ctrl.$error, validationErrorKey, options); + unset(ctrl.$$success, validationErrorKey, options); + } + } + if (ctrl.$pending) { + cachedToggleClass(PENDING_CLASS, true); + ctrl.$valid = ctrl.$invalid = undefined; + toggleValidationCss('', null); + } else { + cachedToggleClass(PENDING_CLASS, false); + ctrl.$valid = isObjectEmpty(ctrl.$error); + ctrl.$invalid = !ctrl.$valid; + toggleValidationCss('', ctrl.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in ctrl.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (ctrl.$error[validationErrorKey]) { + combinedState = false; + } else if (ctrl.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + toggleValidationCss(validationErrorKey, combinedState); + parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + } + + function createAndSet(name, value, options) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, options); + } + + function unsetAndCleanup(name, value, options) { + if (ctrl[name]) { + unset(ctrl[name], value, options); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(className, switchValue) { + if (switchValue && !classCache[className]) { + $animate.addClass($element, className); + classCache[className] = true; + } else if (!switchValue && classCache[className]) { + $animate.removeClass($element, className); + classCache[className] = false; + } + } + + function toggleValidationCss(validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + return false; + } + } + return true; +} + +/** + * @ngdoc directive * @name ngBind * @restrict AC * @@ -17744,7 +21856,7 @@ var ngValueDirective = function() { * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like * `{{ expression }}` which is similar but less verbose. * - * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily + * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an * element attribute, it makes the bindings invisible to the user while the page is loading. * @@ -17757,14 +21869,15 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.name = 'Whirled'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.name = 'Whirled'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Enter name: <input type="text" ng-model="name"><br> Hello <span ng-bind="name"></span>! </div> @@ -17781,15 +21894,21 @@ var ngValueDirective = function() { </file> </example> */ -var ngBindDirective = ngDirective(function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBind); - scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - // We are purposefully using == here rather than === because we want to - // catch when value is "null or undefined" - // jshint -W041 - element.text(value == undefined ? '' : value); - }); -}); +var ngBindDirective = ['$compile', function($compile) { + return { + restrict: 'AC', + compile: function ngBindCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBind); + element = element[0]; + scope.$watch(attr.ngBind, function ngBindWatchAction(value) { + element.textContent = value === undefined ? '' : value; + }); + }; + } + }; +}]; /** @@ -17810,15 +21929,16 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { * * @example * Try it here: enter text in text box and watch the greeting change. - <example> + <example module="bindExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.salutation = 'Hello'; - $scope.name = 'World'; - } + angular.module('bindExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.salutation = 'Hello'; + $scope.name = 'World'; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> Salutation: <input type="text" ng-model="salutation"><br> Name: <input type="text" ng-model="name"><br> <pre ng-bind-template="{{salutation}} {{name}}!"></pre> @@ -17842,14 +21962,19 @@ var ngBindDirective = ngDirective(function(scope, element, attr) { </file> </example> */ -var ngBindTemplateDirective = ['$interpolate', function($interpolate) { - return function(scope, element, attr) { - // TODO: move this to scenario runner - var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); - element.addClass('ng-binding').data('$binding', interpolateFn); - attr.$observe('ngBindTemplate', function(value) { - element.text(value); - }); +var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) { + return { + compile: function ngBindTemplateCompile(templateElement) { + $compile.$$addBindingClass(templateElement); + return function ngBindTemplateLink(scope, element, attr) { + var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate)); + $compile.$$addBindingInfo(element, interpolateFn.expressions); + element = element[0]; + attr.$observe('ngBindTemplate', function(value) { + element.textContent = value === undefined ? '' : value; + }); + }; + } }; }]; @@ -17859,13 +21984,15 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @name ngBindHtml * * @description - * Creates a binding that will innerHTML the result of evaluating the `expression` into the current - * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link - * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize` - * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in - * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to + * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default, + * the resulting HTML content will be sanitized using the {@link ngSanitize.$sanitize $sanitize} service. + * To utilize this functionality, ensure that `$sanitize` is available, for example, by including {@link + * ngSanitize} in your module's dependencies (not in core Angular). In order to use {@link ngSanitize} + * in your module's dependencies, you need to include "angular-sanitize.js" in your application. + * + * You may also bypass sanitization for values you know are safe. To do so, bind to * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example - * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}. + * under {@link ng.$sce#show-me-an-example-using-sce- Strict Contextual Escaping (SCE)}. * * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you * will have an exception (instead of an exploit.) @@ -17874,22 +22001,21 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate. * * @example - Try it here: enter text in text box and watch the greeting change. - <example module="ngBindHtmlExample" deps="angular-sanitize.js"> + <example module="bindHtmlExample" deps="angular-sanitize.js"> <file name="index.html"> - <div ng-controller="ngBindHtmlCtrl"> + <div ng-controller="ExampleController"> <p ng-bind-html="myHTML"></p> </div> </file> <file name="script.js"> - angular.module('ngBindHtmlExample', ['ngSanitize']) - - .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) { - $scope.myHTML = - 'I am an <code>HTML</code>string with <a href="#">links!</a> and other <em>stuff</em>'; - }]); + angular.module('bindHtmlExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.myHTML = + 'I am an <code>HTML</code>string with ' + + '<a href="#">links!</a> and other <em>stuff</em>'; + }]); </file> <file name="protractor.js" type="protractor"> @@ -17900,16 +22026,26 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) { </file> </example> */ -var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) { - return function(scope, element, attr) { - element.addClass('ng-binding').data('$binding', attr.ngBindHtml); +var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) { + return { + restrict: 'A', + compile: function ngBindHtmlCompile(tElement, tAttrs) { + var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { + return (value || '').toString(); + }); + $compile.$$addBindingClass(tElement); - var parsed = $parse(attr.ngBindHtml); - function getStringValue() { return (parsed(scope) || '').toString(); } + return function ngBindHtmlLink(scope, element, attr) { + $compile.$$addBindingInfo(element, attr.ngBindHtml); - scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) { - element.html($sce.getTrustedHtml(parsed(scope)) || ''); - }); + scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { + // we re-evaluate the expr because we want a TrustedValueHolderType + // for $sce, not a string + element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + }); + }; + } }; }]; @@ -17932,7 +22068,7 @@ function classDirective(name, selector) { scope.$watch('$index', function($index, old$index) { // jshint bitwise: false var mod = $index & 1; - if (mod !== old$index & 1) { + if (mod !== (old$index & 1)) { var classes = arrayClasses(scope.$eval(attr[name])); mod === selector ? addClasses(classes) : @@ -17951,10 +22087,10 @@ function classDirective(name, selector) { attr.$removeClass(newClasses); } - function digestClassCounts (classes, count) { + function digestClassCounts(classes, count) { var classCounts = element.data('$classCounts') || {}; var classesToUpdate = []; - forEach(classes, function (className) { + forEach(classes, function(className) { if (count > 0 || classCounts[className]) { classCounts[className] = (classCounts[className] || 0) + count; if (classCounts[className] === +(count > 0)) { @@ -17966,18 +22102,16 @@ function classDirective(name, selector) { return classesToUpdate.join(' '); } - function updateClasses (oldClasses, newClasses) { + function updateClasses(oldClasses, newClasses) { var toAdd = arrayDifference(newClasses, oldClasses); var toRemove = arrayDifference(oldClasses, newClasses); - toRemove = digestClassCounts(toRemove, -1); toAdd = digestClassCounts(toAdd, 1); - - if (toAdd.length === 0) { - $animate.removeClass(element, toRemove); - } else if (toRemove.length === 0) { + toRemove = digestClassCounts(toRemove, -1); + if (toAdd && toAdd.length) { $animate.addClass(element, toAdd); - } else { - $animate.setClass(element, toAdd, toRemove); + } + if (toRemove && toRemove.length) { + $animate.removeClass(element, toRemove); } } @@ -17991,7 +22125,7 @@ function classDirective(name, selector) { updateClasses(oldClasses, newClasses); } } - oldVal = copy(newVal); + oldVal = shallowCopy(newVal); } } }; @@ -18000,26 +22134,26 @@ function classDirective(name, selector) { var values = []; outer: - for(var i = 0; i < tokens1.length; i++) { + for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; - for(var j = 0; j < tokens2.length; j++) { - if(token == tokens2[j]) continue outer; + for (var j = 0; j < tokens2.length; j++) { + if (token == tokens2[j]) continue outer; } values.push(token); } return values; } - function arrayClasses (classVal) { + function arrayClasses(classVal) { if (isArray(classVal)) { return classVal; } else if (isString(classVal)) { return classVal.split(' '); } else if (isObject(classVal)) { - var classes = [], i = 0; + var classes = []; forEach(classVal, function(v, k) { if (v) { - classes.push(k); + classes = classes.concat(k.split(' ')); } }); return classes; @@ -18170,8 +22304,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 ngAnimate.$animate#addclass $animate.addClass} and - {@link ngAnimate.$animate#removeclass $animate.removeClass}. + to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and + {@link ng.$animate#removeClass $animate.removeClass}. */ var ngClassDirective = classDirective('', true); @@ -18344,7 +22478,7 @@ var ngCloakDirective = ngDirective({ * * MVC components in angular: * - * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties + * * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties * are accessed through bindings. * * View — The template (HTML with data bindings) that is rendered into the View. * * Controller — The `ngController` directive specifies a Controller class; the class contains business @@ -18357,177 +22491,212 @@ var ngCloakDirective = ngDirective({ * * @element ANY * @scope - * @param {expression} ngController Name of a globally accessible constructor function or an - * {@link guide/expression expression} that on the current scope evaluates to a - * constructor function. The controller instance can be published into a scope property - * by specifying `as propertyName`. + * @priority 500 + * @param {expression} ngController Name of a constructor function registered with the current + * {@link ng.$controllerProvider $controllerProvider} or an {@link guide/expression expression} + * that on the current scope evaluates to a constructor function. + * + * The controller instance can be published into a scope property by specifying + * `ng-controller="as propertyName"`. + * + * If the current `$controllerProvider` is configured to use globals (via + * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may + * also be the name of a globally accessible constructor function (not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and * greeting are methods declared on the controller (see source tab). These methods can - * easily be called from the angular markup. Notice that the scope becomes the `this` for the - * controller's instance. This allows for easy access to the view data from the controller. Also - * notice that any changes to the data are automatically reflected in the View without the need - * for a manual update. The example is shown in two different declaration styles you may use - * according to preference. - <example> - <file name="index.html"> - <script> - function SettingsController1() { - this.name = "John Smith"; - this.contacts = [ - {type: 'phone', value: '408 555 1212'}, - {type: 'email', value: 'john.smith@example.org'} ]; - }; - - SettingsController1.prototype.greet = function() { - alert(this.name); - }; - - SettingsController1.prototype.addContact = function() { - this.contacts.push({type: 'email', value: 'yourname@example.org'}); - }; - - SettingsController1.prototype.removeContact = function(contactToRemove) { - var index = this.contacts.indexOf(contactToRemove); - this.contacts.splice(index, 1); - }; - - SettingsController1.prototype.clearContact = function(contact) { - contact.type = 'phone'; - contact.value = ''; - }; - </script> - <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/> - Contact: - <ul> - <li ng-repeat="contact in settings.contacts"> - <select ng-model="contact.type"> - <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> ] - </li> - <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> - </ul> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should check controller as', function() { - var container = element(by.id('ctrl-as-exmpl')); - - expect(container.findElement(by.model('settings.name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in settings.contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click(); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in settings.contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - </file> - </example> - <example> - <file name="index.html"> - <script> - function SettingsController2($scope) { - $scope.name = "John Smith"; - $scope.contacts = [ - {type:'phone', value:'408 555 1212'}, - {type:'email', value:'john.smith@example.org'} ]; - - $scope.greet = function() { - alert(this.name); - }; - - $scope.addContact = function() { - this.contacts.push({type:'email', value:'yourname@example.org'}); - }; - - $scope.removeContact = function(contactToRemove) { - var index = this.contacts.indexOf(contactToRemove); - this.contacts.splice(index, 1); - }; - - $scope.clearContact = function(contact) { - contact.type = 'phone'; - contact.value = ''; - }; - } - </script> - <div id="ctrl-exmpl" ng-controller="SettingsController2"> - Name: <input type="text" ng-model="name"/> - [ <a href="" ng-click="greet()">greet</a> ]<br/> - Contact: - <ul> - <li ng-repeat="contact in contacts"> - <select ng-model="contact.type"> - <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> ] - </li> - <li>[ <a href="" ng-click="addContact()">add</a> ]</li> - </ul> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should check controller', function() { - var container = element(by.id('ctrl-exmpl')); - - expect(container.findElement(by.model('name')) - .getAttribute('value')).toBe('John Smith'); - - var firstRepeat = - container.findElement(by.repeater('contact in contacts').row(0)); - var secondRepeat = - container.findElement(by.repeater('contact in contacts').row(1)); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('408 555 1212'); - expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe('john.smith@example.org'); - - firstRepeat.findElement(by.linkText('clear')).click(); - - expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value')) - .toBe(''); - - container.findElement(by.linkText('add')).click(); - - expect(container.findElement(by.repeater('contact in contacts').row(2)) - .findElement(by.model('contact.value')) - .getAttribute('value')) - .toBe('yourname@example.org'); - }); - </file> - </example> + * easily be called from the angular markup. Any changes to the data are automatically reflected + * in the View without the need for a manual update. + * + * Two different declaration styles are included below: + * + * * one binds methods and properties directly onto the controller using `this`: + * `ng-controller="SettingsController1 as settings"` + * * one injects `$scope` into the controller: + * `ng-controller="SettingsController2"` + * + * The second option is more common in the Angular community, and is generally used in boilerplates + * and in this guide. However, there are advantages to binding properties directly to the controller + * and avoiding scope. + * + * * Using `controller as` makes it obvious which controller you are accessing in the template when + * multiple controllers apply to an element. + * * If you are writing your controllers as classes you have easier access to the properties and + * methods, which will appear on the scope, from inside the controller code. + * * Since there is always a `.` in the bindings, you don't have to worry about prototypal + * inheritance masking primitives. + * + * This example demonstrates the `controller as` syntax. + * + * <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/> + * Contact: + * <ul> + * <li ng-repeat="contact in settings.contacts"> + * <select ng-model="contact.type"> + * <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> ] + * </li> + * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li> + * </ul> + * </div> + * </file> + * <file name="app.js"> + * angular.module('controllerAsExample', []) + * .controller('SettingsController1', SettingsController1); + * + * function SettingsController1() { + * this.name = "John Smith"; + * this.contacts = [ + * {type: 'phone', value: '408 555 1212'}, + * {type: 'email', value: 'john.smith@example.org'} ]; + * } + * + * SettingsController1.prototype.greet = function() { + * alert(this.name); + * }; + * + * SettingsController1.prototype.addContact = function() { + * this.contacts.push({type: 'email', value: 'yourname@example.org'}); + * }; + * + * SettingsController1.prototype.removeContact = function(contactToRemove) { + * var index = this.contacts.indexOf(contactToRemove); + * this.contacts.splice(index, 1); + * }; + * + * SettingsController1.prototype.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * </file> + * <file name="protractor.js" type="protractor"> + * it('should check controller as', function() { + * var container = element(by.id('ctrl-as-exmpl')); + * expect(container.element(by.model('settings.name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in settings.contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in settings.contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.linkText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.linkText('add')).click(); + * + * expect(container.element(by.repeater('contact in settings.contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * </file> + * </example> + * + * This example demonstrates the "attach to `$scope`" style of controller. + * + * <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/> + * Contact: + * <ul> + * <li ng-repeat="contact in contacts"> + * <select ng-model="contact.type"> + * <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> ] + * </li> + * <li>[ <a href="" ng-click="addContact()">add</a> ]</li> + * </ul> + * </div> + * </file> + * <file name="app.js"> + * angular.module('controllerExample', []) + * .controller('SettingsController2', ['$scope', SettingsController2]); + * + * function SettingsController2($scope) { + * $scope.name = "John Smith"; + * $scope.contacts = [ + * {type:'phone', value:'408 555 1212'}, + * {type:'email', value:'john.smith@example.org'} ]; + * + * $scope.greet = function() { + * alert($scope.name); + * }; + * + * $scope.addContact = function() { + * $scope.contacts.push({type:'email', value:'yourname@example.org'}); + * }; + * + * $scope.removeContact = function(contactToRemove) { + * var index = $scope.contacts.indexOf(contactToRemove); + * $scope.contacts.splice(index, 1); + * }; + * + * $scope.clearContact = function(contact) { + * contact.type = 'phone'; + * contact.value = ''; + * }; + * } + * </file> + * <file name="protractor.js" type="protractor"> + * it('should check controller', function() { + * var container = element(by.id('ctrl-exmpl')); + * + * expect(container.element(by.model('name')) + * .getAttribute('value')).toBe('John Smith'); + * + * var firstRepeat = + * container.element(by.repeater('contact in contacts').row(0)); + * var secondRepeat = + * container.element(by.repeater('contact in contacts').row(1)); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('408 555 1212'); + * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe('john.smith@example.org'); + * + * firstRepeat.element(by.linkText('clear')).click(); + * + * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value')) + * .toBe(''); + * + * container.element(by.linkText('add')).click(); + * + * expect(container.element(by.repeater('contact in contacts').row(2)) + * .element(by.model('contact.value')) + * .getAttribute('value')) + * .toBe('yourname@example.org'); + * }); + * </file> + *</example> */ var ngControllerDirective = [function() { return { + restrict: 'A', scope: true, controller: '@', priority: 500 @@ -18542,11 +22711,13 @@ var ngControllerDirective = [function() { * @description * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support. * - * This is necessary when developing things like Google Chrome Extensions. + * 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 us to be compatible, we just need to implement the "getterFn" in $parse without violating - * any of these restrictions. + * 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 * * 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 @@ -18557,7 +22728,18 @@ var ngControllerDirective = [function() { * 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. * - * In order to use this feature put the `ngCsp` directive on the root element of the application. + * 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: + * + * ``` + * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of + * script in the following Content Security Policy directive: "default-src 'self'". Note that + * 'script-src' was not explicitly set, so 'default-src' is used as a fallback. + * ``` + * + * 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. * * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.* * @@ -18570,11 +22752,129 @@ var ngControllerDirective = [function() { ... </html> ``` - */ + * @example + // Note: the suffix `.csp` in the example name triggers + // csp mode in our http server! + <example name="example.csp" module="cspExample" ng-csp="true"> + <file name="index.html"> + <div ng-controller="MainController as ctrl"> + <div> + <button ng-click="ctrl.inc()" id="inc">Increment</button> + <span id="counter"> + {{ctrl.counter}} + </span> + </div> + + <div> + <button ng-click="ctrl.evil()" id="evil">Evil</button> + <span id="evilError"> + {{ctrl.evilError}} + </span> + </div> + </div> + </file> + <file name="script.js"> + angular.module('cspExample', []) + .controller('MainController', function() { + this.counter = 0; + this.inc = function() { + this.counter++; + }; + this.evil = function() { + // jshint evil:true + try { + eval('1+2'); + } catch (e) { + this.evilError = e.message; + } + }; + }); + </file> + <file name="protractor.js" type="protractor"> + var util, webdriver; + + var incBtn = element(by.id('inc')); + var counter = element(by.id('counter')); + var evilBtn = element(by.id('evil')); + var evilError = element(by.id('evilError')); + + function getAndClearSevereErrors() { + return browser.manage().logs().get('browser').then(function(browserLog) { + return browserLog.filter(function(logEntry) { + return logEntry.level.value > webdriver.logging.Level.WARNING.value; + }); + }); + } + + function clearErrors() { + getAndClearSevereErrors(); + } + + function expectNoErrors() { + getAndClearSevereErrors().then(function(filteredLog) { + expect(filteredLog.length).toEqual(0); + if (filteredLog.length) { + console.log('browser console errors: ' + util.inspect(filteredLog)); + } + }); + } + + function expectError(regex) { + getAndClearSevereErrors().then(function(filteredLog) { + var found = false; + filteredLog.forEach(function(log) { + if (log.message.match(regex)) { + found = true; + } + }); + if (!found) { + throw new Error('expected an error that matches ' + regex); + } + }); + } + + beforeEach(function() { + util = require('util'); + webdriver = require('protractor/node_modules/selenium-webdriver'); + }); + + // For now, we only test on Chrome, + // as Safari does not load the page with Protractor's injected scripts, + // and Firefox webdriver always disables content security policy (#6358) + if (browser.params.browser !== 'chrome') { + return; + } + + it('should not report errors when the page is loaded', function() { + // clear errors so we are not dependent on previous tests + clearErrors(); + // Need to reload the page as the page is already loaded when + // we come here + browser.driver.getCurrentUrl().then(function(url) { + browser.get(url); + }); + expectNoErrors(); + }); + + it('should evaluate expressions', function() { + expect(counter.getText()).toEqual('0'); + incBtn.click(); + expect(counter.getText()).toEqual('1'); + expectNoErrors(); + }); + + it('should throw and report an error when using "eval"', function() { + evilBtn.click(); + expect(evilError.getText()).toMatch(/Content Security Policy/); + expectError(/Content Security Policy/); + }); + </file> + </example> + */ -// 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 a csp() fn that looks for ng-csp attribute -// anywhere in the current doc +// 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 /** * @ngdoc directive @@ -18595,7 +22895,9 @@ var ngControllerDirective = [function() { <button ng-click="count = count + 1" ng-init="count=0"> Increment </button> - count: {{count}} + <span> + count: {{count}} + </span> </file> <file name="protractor.js" type="protractor"> it('should check ng-click', function() { @@ -18607,25 +22909,41 @@ var ngControllerDirective = [function() { </example> */ /* - * A directive that allows creation of custom onclick handlers that are defined as angular - * expressions and are compiled and executed within the current scope. - * - * Events that are handled via these handler are always configured not to propagate further. + * A collection of directives that allows creation of custom event handlers that are defined as + * angular expressions and are compiled and executed within the current scope. */ var ngEventDirectives = {}; + +// For events that might fire synchronously during DOM manipulation +// we need to execute their event handlers asynchronously using $evalAsync, +// so that they are not executed in an inconsistent state. +var forceAsyncEvents = { + 'blur': true, + 'focus': true +}; forEach( 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), - function(name) { - var directiveName = directiveNormalize('ng-' + name); - ngEventDirectives[directiveName] = ['$parse', function($parse) { + function(eventName) { + var directiveName = directiveNormalize('ng-' + eventName); + ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) { return { + restrict: 'A', compile: function($element, attr) { - var fn = $parse(attr[directiveName]); - return function(scope, element, attr) { - element.on(lowercase(name), function(event) { - scope.$apply(function() { + // We expose the powerful $event object on the scope that provides access to the Window, + // etc. that isn't protected by the fast paths in $parse. We explicitly request better + // checks at the cost of speed since event handler expressions are not executed as + // frequently as regular change detection. + var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); + return function ngEventHandler(scope, element) { + element.on(eventName, function(event) { + var callback = function() { fn(scope, {$event:event}); - }); + }; + if (forceAsyncEvents[eventName] && $rootScope.$$phase) { + scope.$evalAsync(callback); + } else { + scope.$apply(callback); + } }); }; } @@ -18838,8 +23156,13 @@ forEach( * @example <example> <file name="index.html"> - <input ng-keyup="count = count + 1" ng-init="count=0"> - key up count: {{count}} + <p>Typing in the input box below updates the key count</p> + <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}} + + <p>Typing in the input box below updates the keycode</p> + <input ng-keyup="event=$event"> + <p>event keyCode: {{ event.keyCode }}</p> + <p>event altKey: {{ event.altKey }}</p> </file> </example> */ @@ -18878,27 +23201,35 @@ forEach( * server and reloading the current page), but only if the form does not contain `action`, * `data-action`, or `x-action` attributes. * + * <div class="alert alert-warning"> + * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and + * `ngSubmit` handlers together. See the + * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation} + * for a detailed discussion of when `ngSubmit` may be triggered. + * </div> + * * @element form * @priority 0 * @param {expression} ngSubmit {@link guide/expression Expression} to eval. * ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example module="submitExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = []; - $scope.text = 'hello'; - $scope.submit = function() { - if ($scope.text) { - $scope.list.push(this.text); - $scope.text = ''; - } - }; - } + angular.module('submitExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = []; + $scope.text = 'hello'; + $scope.submit = function() { + if ($scope.text) { + $scope.list.push(this.text); + $scope.text = ''; + } + }; + }]); </script> - <form ng-submit="submit()" ng-controller="Ctrl"> + <form ng-submit="submit()" ng-controller="ExampleController"> Enter text and hit enter: <input type="text" ng-model="text" name="text" /> <input type="submit" id="submit" value="Submit" /> @@ -18910,7 +23241,7 @@ forEach( expect(element(by.binding('list')).getText()).toBe('list=[]'); element(by.css('#submit')).click(); expect(element(by.binding('list')).getText()).toContain('hello'); - expect(element(by.input('text')).getAttribute('value')).toBe(''); + expect(element(by.model('text')).getAttribute('value')).toBe(''); }); it('should ignore empty strings', function() { expect(element(by.binding('list')).getText()).toBe('list=[]'); @@ -18929,6 +23260,10 @@ forEach( * @description * Specify custom behavior on focus event. * + * Note: As the `focus` event is executed synchronously when calling `input.focus()` + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon @@ -18945,6 +23280,14 @@ forEach( * @description * Specify custom behavior on blur event. * + * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when + * an element has lost focus. + * + * Note: As the `blur` event is executed synchronously also during DOM manipulations + * (e.g. removing a focussed input), + * AngularJS executes the expression using `scope.$evalAsync` if the event is fired + * during an `$apply` to ensure a consistent state. + * * @element window, input, select, textarea, a * @priority 0 * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon @@ -19036,7 +23379,7 @@ forEach( * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope * is created when the element is restored. The scope created within `ngIf` inherits from * its parent scope using - * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance). + * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance). * An important implication of this is if `ngModel` is used within `ngIf` to bind to * a javascript primitive defined in the parent scope. In this case any modifications made to the * variable within the child scope will override (hide) the value in the parent scope. @@ -19050,8 +23393,8 @@ forEach( * and `leave` effects. * * @animations - * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container - * leave - happens just before the ngIf contents are removed from the DOM + * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container + * leave - happens just before the `ngIf` contents are removed from the DOM * * @element ANY * @scope @@ -19066,7 +23409,7 @@ forEach( Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/> Show when checked: <span ng-if="checked" class="animate-if"> - I'm removed when the checkbox is unchecked. + This is removed when the checkbox is unchecked. </span> </file> <file name="animations.css"> @@ -19095,23 +23438,24 @@ forEach( */ var ngIfDirective = ['$animate', function($animate) { return { + multiElement: true, transclude: 'element', priority: 600, terminal: true, restrict: 'A', $$tlb: true, - link: function ($scope, $element, $attr, ctrl, $transclude) { + link: function($scope, $element, $attr, ctrl, $transclude) { var block, childScope, previousElements; $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { - if (toBoolean(value)) { + if (value) { if (!childScope) { - childScope = $scope.$new(); - $transclude(childScope, function (clone) { + $transclude(function(clone, newScope) { + childScope = newScope; clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); // 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 - // by a directive with templateUrl when it's template arrives. + // by a directive with templateUrl when its template arrives. block = { clone: clone }; @@ -19119,17 +23463,17 @@ var ngIfDirective = ['$animate', function($animate) { }); } } else { - if(previousElements) { + if (previousElements) { previousElements.remove(); previousElements = null; } - if(childScope) { + if (childScope) { childScope.$destroy(); childScope = null; } - if(block) { - previousElements = getBlockElements(block.clone); - $animate.leave(previousElements, function() { + if (block) { + previousElements = getBlockNodes(block.clone); + $animate.leave(previousElements).then(function() { previousElements = null; }); block = null; @@ -19149,10 +23493,10 @@ var ngIfDirective = ['$animate', function($animate) { * Fetches, compiles and includes an external HTML fragment. * * By default, the template URL is restricted to the same domain and protocol as the - * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl + * application document. This is done by calling {@link $sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or - * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link + * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to Angular's {@link * ng.$sce Strict Contextual Escaping}. * * In addition, the browser's @@ -19183,9 +23527,9 @@ var ngIfDirective = ['$animate', function($animate) { * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="includeExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="template" ng-options="t.name for t in templates"> <option value="">(blank)</option> </select> @@ -19197,12 +23541,13 @@ var ngIfDirective = ['$animate', function($animate) { </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.templates = - [ { name: 'template1.html', url: 'template1.html'}, - { name: 'template2.html', url: 'template2.html'} ]; - $scope.template = $scope.templates[0]; - } + angular.module('includeExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.templates = + [ { name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'} ]; + $scope.template = $scope.templates[0]; + }]); </file> <file name="template1.html"> Content of template1.html @@ -19265,7 +23610,7 @@ var ngIfDirective = ['$animate', function($animate) { return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(2).click(); + templateSelect.all(by.css('option')).get(2).click(); expect(includeElem.getText()).toMatch(/Content of template2.html/); }); @@ -19275,7 +23620,7 @@ var ngIfDirective = ['$animate', function($animate) { return; } templateSelect.click(); - templateSelect.element.all(by.css('option')).get(0).click(); + templateSelect.all(by.css('option')).get(0).click(); expect(includeElem.isPresent()).toBe(false); }); </file> @@ -19289,6 +23634,9 @@ var ngIfDirective = ['$animate', function($animate) { * @eventType emit on the scope ngInclude was declared in * @description * Emitted every time the ngInclude content is requested. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ @@ -19298,9 +23646,24 @@ var ngIfDirective = ['$animate', function($animate) { * @eventType emit on the current ngInclude scope * @description * Emitted every time the ngInclude content is reloaded. + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $animate, $sce) { + + +/** + * @ngdoc event + * @name ngInclude#$includeContentError + * @eventType emit on the scope ngInclude was declared in + * @description + * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299) + * + * @param {Object} angularEvent Synthetic event object. + * @param {String} src URL of content to load. + */ +var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce', + function($templateRequest, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', priority: 400, @@ -19319,16 +23682,16 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' currentElement; var cleanupLastIncludeContent = function() { - if(previousElement) { + if (previousElement) { previousElement.remove(); previousElement = null; } - if(currentScope) { + if (currentScope) { currentScope.$destroy(); currentScope = null; } - if(currentElement) { - $animate.leave(currentElement, function() { + if (currentElement) { + $animate.leave(currentElement).then(function() { previousElement = null; }); previousElement = currentElement; @@ -19345,7 +23708,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' var thisChangeId = ++changeCounter; if (src) { - $http.get(src, {cache: $templateCache}).success(function(response) { + //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 (thisChangeId !== changeCounter) return; var newScope = scope.$new(); ctrl.template = response; @@ -19358,18 +23723,21 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate' // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element, afterAnimation); + $animate.enter(clone, null, $element).then(afterAnimation); }); currentScope = newScope; currentElement = clone; - currentScope.$emit('$includeContentLoaded'); + currentScope.$emit('$includeContentLoaded', src); scope.$eval(onloadExp); - }).error(function() { - if (thisChangeId === changeCounter) cleanupLastIncludeContent(); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } }); - scope.$emit('$includeContentRequested'); + scope.$emit('$includeContentRequested', src); } else { cleanupLastIncludeContent(); ctrl.template = null; @@ -19392,6 +23760,18 @@ var ngIncludeFillContentDirective = ['$compile', priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { + if (/SVG/.test($element[0].toString())) { + // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not + // support innerHTML, so detect this here and try to generate the contents + // specially. + $element.empty(); + $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope, + function namespaceAdaptedClone(clone) { + $element.append(clone); + }, {futureParentElement: $element}); + return; + } + $element.html(ctrl.template); $compile($element.contents())(scope); } @@ -19427,14 +23807,15 @@ var ngIncludeFillContentDirective = ['$compile', * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - <example> + <example module="initExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.list = [['a', 'b'], ['c', 'd']]; - } + angular.module('initExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.list = [['a', 'b'], ['c', 'd']]; + }]); </script> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <div ng-repeat="innerList in list" ng-init="outerIndex = $index"> <div ng-repeat="value in innerList" ng-init="innerIndex = $index"> <span class="example-init">list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};</span> @@ -19574,7 +23955,7 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * When one person, perhaps John, views the document, "John is viewing" will be shown. * When three people view the document, no explicit number rule is found, so * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. - * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing" * is shown. * * Note that when you specify offsets, you must provide explicit number rules for @@ -19587,16 +23968,17 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); * @param {number=} offset Offset to deduct from the total number. * * @example - <example> + <example module="pluralizeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.person1 = 'Igor'; - $scope.person2 = 'Misko'; - $scope.personCount = 1; - } + angular.module('pluralizeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.person1 = 'Igor'; + $scope.person2 = 'Misko'; + $scope.personCount = 1; + }]); </script> - <div ng-controller="Ctrl"> + <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/> @@ -19670,7 +24052,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); </example> */ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) { - var BRACE = /{}/g; + var BRACE = /{}/g, + IS_WHEN = /^when(Minus)?(.+)$/; + return { restrict: 'EA', link: function(scope, element, attr) { @@ -19681,34 +24065,44 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp whensExpFns = {}, startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - isWhen = /^when(Minus)?(.+)$/; + braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol, + watchRemover = angular.noop, + lastCount; forEach(attr, function(expression, attributeName) { - if (isWhen.test(attributeName)) { - whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] = - element.attr(attr.$attr[attributeName]); + var tmpMatch = IS_WHEN.exec(attributeName); + if (tmpMatch) { + var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]); + whens[whenKey] = element.attr(attr.$attr[attributeName]); } }); forEach(whens, function(expression, key) { - whensExpFns[key] = - $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' + - offset + endSymbol)); + whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement)); + }); - scope.$watch(function ngPluralizeWatch() { - var value = parseFloat(scope.$eval(numberExp)); + scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) { + var count = parseFloat(newVal); + var countIsNaN = isNaN(count); - if (!isNaN(value)) { - //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise, - //check it against pluralization rules in $locale service - if (!(value in whens)) value = $locale.pluralCat(value - offset); - return whensExpFns[value](scope, element, true); - } else { - return ''; + if (!countIsNaN && !(count in whens)) { + // If an explicit number rule such as 1, 2, 3... is defined, just use it. + // Otherwise, check it against pluralization rules in $locale service. + count = $locale.pluralCat(count - offset); + } + + // 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))) { + watchRemover(); + watchRemover = scope.$watch(whensExpFns[count], updateElementText); + lastCount = count; } - }, function ngPluralizeWatchAction(newVal) { - element.text(newVal); }); + + function updateElementText(newText) { + element.text(newText || ''); + } } }; }]; @@ -19810,7 +24204,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression, * before specifying a tracking expression. * - * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements + * 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. * * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique @@ -19825,6 +24219,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter * to items in conjunction with a tracking expression. * + * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the + * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message + * when a filter is active on the repeater, but the filtered result set is empty. + * + * 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. + * * @example * This example initializes the scope to a list of names and * then uses `ngRepeat` to display every person: @@ -19845,9 +24246,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." /> <ul class="example-animate-container"> - <li class="animate-repeat" ng-repeat="friend in friends | filter:q"> + <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> + <li class="animate-repeat" ng-if="results.length == 0"> + <strong>No results found...</strong> + </li> </ul> </div> </file> @@ -19914,29 +24318,84 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); + + var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) { + // TODO(perf): generate setters to shave off ~40ms or 1-1.5% + scope[valueIdentifier] = value; + if (keyIdentifier) scope[keyIdentifier] = key; + scope.$index = index; + scope.$first = (index === 0); + scope.$last = (index === (arrayLength - 1)); + scope.$middle = !(scope.$first || scope.$last); + // jshint bitwise: false + scope.$odd = !(scope.$even = (index&1) === 0); + // jshint bitwise: true + }; + + var getBlockStart = function(block) { + return block.clone[0]; + }; + + var getBlockEnd = function(block) { + return block.clone[block.clone.length - 1]; + }; + + return { + restrict: 'A', + multiElement: true, transclude: 'element', priority: 1000, terminal: true, $$tlb: true, - link: function($scope, $element, $attr, ctrl, $transclude){ - var expression = $attr.ngRepeat; - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/), - trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn, - lhs, rhs, valueIdentifier, keyIdentifier, - hashFnLocals = {$id: hashKey}; - - if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + compile: function ngRepeatCompile($element, $attr) { + var expression = $attr.ngRepeat; + var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + if (!match) { + throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", expression); - } + } + + var lhs = match[1]; + var rhs = match[2]; + var aliasAs = match[3]; + var trackByExp = match[4]; - lhs = match[1]; - rhs = match[2]; - trackByExp = match[3]; + match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/); - if (trackByExp) { - trackByExpGetter = $parse(trackByExp); + if (!match) { + throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + lhs); + } + var valueIdentifier = match[3] || match[1]; + var keyIdentifier = match[2]; + + if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || + /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(aliasAs))) { + throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.", + aliasAs); + } + + var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn; + var hashFnLocals = {$id: hashKey}; + + if (trackByExp) { + trackByExpGetter = $parse(trackByExp); + } else { + trackByIdArrayFn = function(key, value) { + return hashKey(value); + }; + trackByIdObjFn = function(key) { + return key; + }; + } + + return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) { + + if (trackByExpGetter) { trackByIdExpFn = function(key, value, index) { // assign key, value, and $index to the locals so that they can be used in hash functions if (keyIdentifier) hashFnLocals[keyIdentifier] = key; @@ -19944,48 +24403,39 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { hashFnLocals.$index = index; return trackByExpGetter($scope, hashFnLocals); }; - } else { - trackByIdArrayFn = function(key, value) { - return hashKey(value); - }; - trackByIdObjFn = function(key) { - return key; - }; - } - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); } - valueIdentifier = match[3] || match[1]; - keyIdentifier = match[2]; // Store a list of elements from previous run. This is a hash where key is the item from the // iterator, and the value is objects with following properties. // - scope: bound scope // - element: previous element. // - index: position - var lastBlockMap = {}; + // + // We are using no-proto object so that we don't need to guard against inherited props via + // hasOwnProperty. + var lastBlockMap = createMap(); //watch props - $scope.$watchCollection(rhs, function ngRepeatAction(collection){ + $scope.$watchCollection(rhs, function ngRepeatAction(collection) { var index, length, - previousNode = $element[0], // current position of the node + previousNode = $element[0], // node that cloned nodes should be inserted after + // initialized to the comment node anchor nextNode, // Same as lastBlockMap but it has the current state. It will become the // lastBlockMap on the next iteration. - nextBlockMap = {}, - arrayLength, - childScope, + nextBlockMap = createMap(), + collectionLength, key, value, // key/value of iteration trackById, trackByIdFn, collectionKeys, block, // last object information {scope, element, id} - nextBlockOrder = [], + nextBlockOrder, elementsToRemove; + if (aliasAs) { + $scope[aliasAs] = collection; + } if (isArrayLike(collection)) { collectionKeys = collection; @@ -19994,128 +24444,118 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { trackByIdFn = trackByIdExpFn || trackByIdObjFn; // if object, extract keys, sort them and use to determine order of iteration over obj props collectionKeys = []; - for (key in collection) { - if (collection.hasOwnProperty(key) && key.charAt(0) != '$') { - collectionKeys.push(key); + for (var itemKey in collection) { + if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') { + collectionKeys.push(itemKey); } } collectionKeys.sort(); } - arrayLength = collectionKeys.length; + collectionLength = collectionKeys.length; + nextBlockOrder = new Array(collectionLength); // locate existing items - length = nextBlockOrder.length = collectionKeys.length; - for(index = 0; index < length; index++) { - key = (collection === collectionKeys) ? index : collectionKeys[index]; - value = collection[key]; - trackById = trackByIdFn(key, value, index); - assertNotHasOwnProperty(trackById, '`track by` id'); - if(lastBlockMap.hasOwnProperty(trackById)) { - block = lastBlockMap[trackById]; - delete lastBlockMap[trackById]; - nextBlockMap[trackById] = block; - nextBlockOrder[index] = block; - } else if (nextBlockMap.hasOwnProperty(trackById)) { - // restore lastBlockMap - forEach(nextBlockOrder, function(block) { - if (block && block.scope) lastBlockMap[block.id] = block; - }); - // This is a duplicate and we need to throw an error - throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}", - expression, trackById); - } else { - // new never before seen block - nextBlockOrder[index] = { id: trackById }; - nextBlockMap[trackById] = false; - } - } + for (index = 0; index < collectionLength; index++) { + key = (collection === collectionKeys) ? index : collectionKeys[index]; + value = collection[key]; + trackById = trackByIdFn(key, value, index); + if (lastBlockMap[trackById]) { + // found previously seen block + block = lastBlockMap[trackById]; + delete lastBlockMap[trackById]; + nextBlockMap[trackById] = block; + nextBlockOrder[index] = block; + } else if (nextBlockMap[trackById]) { + // if collision detected. restore lastBlockMap and throw an error + forEach(nextBlockOrder, function(block) { + if (block && block.scope) lastBlockMap[block.id] = block; + }); + throw ngRepeatMinErr('dupes', + "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", + expression, trackById, value); + } else { + // new never before seen block + nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; + nextBlockMap[trackById] = true; + } + } - // remove existing items - for (key in lastBlockMap) { - // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn - if (lastBlockMap.hasOwnProperty(key)) { - block = lastBlockMap[key]; - elementsToRemove = getBlockElements(block.clone); - $animate.leave(elementsToRemove); - forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; }); - block.scope.$destroy(); + // remove leftover items + for (var blockKey in lastBlockMap) { + block = lastBlockMap[blockKey]; + elementsToRemove = getBlockNodes(block.clone); + $animate.leave(elementsToRemove); + if (elementsToRemove[0].parentNode) { + // if the element was not removed yet because of pending animation, mark it as deleted + // so that we can ignore it later + for (index = 0, length = elementsToRemove.length; index < length; index++) { + elementsToRemove[index][NG_REMOVED] = true; + } } + block.scope.$destroy(); } // we are not using forEach for perf reasons (trying to avoid #call) - for (index = 0, length = collectionKeys.length; index < length; index++) { + for (index = 0; index < collectionLength; index++) { key = (collection === collectionKeys) ? index : collectionKeys[index]; value = collection[key]; block = nextBlockOrder[index]; - if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]); if (block.scope) { // if we have already seen this object, then we need to reuse the // associated scope/element - childScope = block.scope; nextNode = previousNode; + + // skip nodes that are already pending removal via leave animation do { nextNode = nextNode.nextSibling; - } while(nextNode && nextNode[NG_REMOVED]); + } while (nextNode && nextNode[NG_REMOVED]); if (getBlockStart(block) != nextNode) { // existing item which got moved - $animate.move(getBlockElements(block.clone), null, jqLite(previousNode)); + $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode)); } previousNode = getBlockEnd(block); + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); } else { // new item which we don't know about - childScope = $scope.$new(); - } - - childScope[valueIdentifier] = value; - if (keyIdentifier) childScope[keyIdentifier] = key; - childScope.$index = index; - childScope.$first = (index === 0); - childScope.$last = (index === (arrayLength - 1)); - childScope.$middle = !(childScope.$first || childScope.$last); - // jshint bitwise: false - childScope.$odd = !(childScope.$even = (index&1) === 0); - // jshint bitwise: true + $transclude(function ngRepeatTransclude(clone, scope) { + block.scope = scope; + // http://jsperf.com/clone-vs-createcomment + var endNode = ngRepeatEndComment.cloneNode(false); + clone[clone.length++] = endNode; - if (!block.scope) { - $transclude(childScope, function(clone) { - clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' '); + // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper? $animate.enter(clone, null, jqLite(previousNode)); - previousNode = clone; - block.scope = childScope; + 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 - // by a directive with templateUrl when it's template arrives. + // by a directive with templateUrl when its template arrives. block.clone = clone; nextBlockMap[block.id] = block; + updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); }); } } lastBlockMap = nextBlockMap; }); + }; } }; - - function getBlockStart(block) { - return block.clone[0]; - } - - function getBlockEnd(block) { - return block.clone[block.clone.length - 1]; - } }]; +var NG_HIDE_CLASS = 'ng-hide'; +var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; /** * @ngdoc directive * @name ngShow * * @description * The `ngShow` directive shows or hides the given HTML element based on the expression - * provided to the ngShow attribute. The element is shown or hidden by removing or adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined + * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding + * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined * in AngularJS and sets the display style to none (using an !important flag). * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * @@ -20127,13 +24567,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When true, the ng-hide CSS class is removed + * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed * from the element causing the element not to appear hidden. * * ## Why is !important used? * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. * This also becomes a bigger issue when dealing with CSS frameworks. @@ -20142,30 +24582,25 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. * - * ### Overriding .ng-hide + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: * ```css * .ng-hide { - * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default... - * display:block!important; - * - * //this is just another form of hiding an element - * position:absolute; - * top:-9999px; - * left:-9999px; + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; * } * ``` * - * Just remember to include the important flag so the CSS override will function. + * By default you don't need to override in CSS anything and the animations will work around the display style. * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> - * - * ## A note about animations with ngShow + * ## A note about animations with `ngShow` * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression * is true and false. This system works like the animation system present with ngClass except that @@ -20177,8 +24612,15 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * //a working example can be found at the bottom of this page * // * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition:0.5s linear all; - * display:block!important; + * /* this is required as of 1.3x to properly + * apply all styling in a show/hide animation */ + * transition: 0s linear all; + * } + * + * .my-element.ng-hide-add-active, + * .my-element.ng-hide-remove-active { + * /* the transition is defined in the active class */ + * transition: 1s linear all; * } * * .my-element.ng-hide-add { ... } @@ -20187,9 +24629,12 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * * @animations - * addClass: .ng-hide - happens after the ngShow expression evaluates to a truthy value and the just before contents are set to visible - * removeClass: .ng-hide - happens after the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden * * @element ANY * @param {expression} ngShow If the {@link guide/expression expression} is truthy @@ -20213,34 +24658,33 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </div> </file> <file name="glyphicons.css"> - @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> .animate-show { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; } - .animate-show.ng-hide-add, - .animate-show.ng-hide-remove { - display:block!important; + .animate-show.ng-hide-add.ng-hide-add-active, + .animate-show.ng-hide-remove.ng-hide-remove-active { + -webkit-transition: all linear 0.5s; + transition: all linear 0.5s; } .animate-show.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; + line-height: 0; + opacity: 0; + padding: 0 10px; } .check-element { - padding:10px; - border:1px solid black; - background:white; + padding: 10px; + border: 1px solid black; + background: white; } </file> <file name="protractor.js" type="protractor"> @@ -20260,10 +24704,20 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { </example> */ var ngShowDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngShow, function ngShowWatchAction(value){ - $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngShow, function ngShowWatchAction(value) { + // we're adding a temporary, animation-specific class for ng-hide since this way + // we can control when the element is actually displayed on screen without having + // to have a global/greedy CSS selector that breaks when other animations are run. + // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845 + $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } }; }]; @@ -20274,26 +24728,26 @@ var ngShowDirective = ['$animate', function($animate) { * * @description * The `ngHide` directive shows or hides the given HTML element based on the expression - * provided to the ngHide attribute. The element is shown or hidden by removing or adding + * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined * in AngularJS and sets the display style to none (using an !important flag). * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). * * ```html * <!-- when $scope.myValue is truthy (element is hidden) --> - * <div ng-hide="myValue"></div> + * <div ng-hide="myValue" class="ng-hide"></div> * * <!-- when $scope.myValue is falsy (element is visible) --> - * <div ng-hide="myValue" class="ng-hide"></div> + * <div ng-hide="myValue"></div> * ``` * - * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute - * on the element causing it to become hidden. When false, the ng-hide CSS class is removed + * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class + * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed * from the element causing the element not to appear hidden. * * ## Why is !important used? * - * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector + * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector * can be easily overridden by heavier selectors. For example, something as simple * as changing the display style on a HTML list item would make hidden elements appear visible. * This also becomes a bigger issue when dealing with CSS frameworks. @@ -20302,43 +24756,36 @@ var ngShowDirective = ['$animate', function($animate) { * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. * - * ### Overriding .ng-hide + * ### Overriding `.ng-hide` + * + * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change + * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` + * class in CSS: * - * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by - * restating the styles for the .ng-hide class in CSS: * ```css * .ng-hide { - * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default... - * display:block!important; - * - * //this is just another form of hiding an element - * position:absolute; - * top:-9999px; - * left:-9999px; + * /* this is just another form of hiding an element */ + * display: block!important; + * position: absolute; + * top: -9999px; + * left: -9999px; * } * ``` * - * Just remember to include the important flag so the CSS override will function. - * - * <div class="alert alert-warning"> - * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):<br /> - * "f" / "0" / "false" / "no" / "n" / "[]" - * </div> + * By default you don't need to override in CSS anything and the animations will work around the display style. * - * ## A note about animations with ngHide + * ## A note about animations with `ngHide` * * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass, except that - * you must also include the !important flag to override the display property so - * that you can perform an animation when the element is hidden during the time of the animation. + * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` + * CSS class is added and removed for you instead of your own CSS class. * * ```css * // * //a working example can be found at the bottom of this page * // * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition:0.5s linear all; - * display:block!important; + * transition: 0.5s linear all; * } * * .my-element.ng-hide-add { ... } @@ -20347,9 +24794,12 @@ var ngShowDirective = ['$animate', function($animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * + * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display + * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * * @animations - * removeClass: .ng-hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden - * addClass: .ng-hide - happens after the ngHide expression evaluates to a non truthy value and just before the contents are set to visible + * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden + * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible * * @element ANY * @param {expression} ngHide If the {@link guide/expression expression} is truthy then @@ -20373,34 +24823,29 @@ var ngShowDirective = ['$animate', function($animate) { </div> </file> <file name="glyphicons.css"> - @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css); + @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> .animate-hide { - -webkit-transition:all linear 0.5s; - transition:all linear 0.5s; - line-height:20px; - opacity:1; - padding:10px; - border:1px solid black; - background:white; - } - - .animate-hide.ng-hide-add, - .animate-hide.ng-hide-remove { - display:block!important; + -webkit-transition: all linear 0.5s; + transition: all linear 0.5s; + line-height: 20px; + opacity: 1; + padding: 10px; + border: 1px solid black; + background: white; } .animate-hide.ng-hide { - line-height:0; - opacity:0; - padding:0 10px; + line-height: 0; + opacity: 0; + padding: 0 10px; } .check-element { - padding:10px; - border:1px solid black; - background:white; + padding: 10px; + border: 1px solid black; + background: white; } </file> <file name="protractor.js" type="protractor"> @@ -20420,10 +24865,18 @@ var ngShowDirective = ['$animate', function($animate) { </example> */ var ngHideDirective = ['$animate', function($animate) { - return function(scope, element, attr) { - scope.$watch(attr.ngHide, function ngHideWatchAction(value){ - $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide'); - }); + return { + restrict: 'A', + multiElement: true, + link: function(scope, element, attr) { + scope.$watch(attr.ngHide, function ngHideWatchAction(value) { + // The comment inside of the ngShowDirective explains why we add and + // remove a temporary class for the show/hide animation + $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, { + tempClasses: NG_HIDE_IN_PROGRESS_CLASS + }); + }); + } }; }]; @@ -20436,14 +24889,20 @@ var ngHideDirective = ['$animate', function($animate) { * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. * * @element ANY - * @param {expression} ngStyle {@link guide/expression Expression} which evals to an - * object whose keys are CSS style names and values are corresponding values for those CSS - * keys. + * @param {expression} ngStyle + * + * {@link guide/expression Expression} which evals to an + * object whose keys are CSS style names and values are corresponding values for those CSS + * keys. + * + * Since some CSS style names are not valid keys for an object, they must be quoted. + * See the 'background-color' style in the example below. * * @example <example> <file name="index.html"> - <input type="button" value="set" ng-click="myStyle={color:'red'}"> + <input type="button" value="set color" ng-click="myStyle={color:'red'}"> + <input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}"> <input type="button" value="clear" ng-click="myStyle={}"> <br/> <span ng-style="myStyle">Sample Text</span> @@ -20459,7 +24918,7 @@ var ngHideDirective = ['$animate', function($animate) { it('should check ng-style', function() { expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); - element(by.css('input[value=set]')).click(); + element(by.css('input[value=\'set color\']')).click(); expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)'); element(by.css('input[value=clear]')).click(); expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)'); @@ -20507,15 +24966,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM * * @usage + * + * ``` * <ANY ng-switch="expression"> * <ANY ng-switch-when="matchValue1">...</ANY> * <ANY ng-switch-when="matchValue2">...</ANY> * <ANY ng-switch-default>...</ANY> * </ANY> + * ``` * * * @scope - * @priority 800 + * @priority 1200 * @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>. * On child elements add: * @@ -20528,9 +24990,9 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="switchExample" deps="angular-animate.js" animations="true"> <file name="index.html"> - <div ng-controller="Ctrl"> + <div ng-controller="ExampleController"> <select ng-model="selection" ng-options="item for item in items"> </select> <tt>selection={{selection}}</tt> @@ -20544,10 +25006,11 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { </div> </file> <file name="script.js"> - function Ctrl($scope) { - $scope.items = ['settings', 'home', 'other']; - $scope.selection = $scope.items[0]; - } + angular.module('switchExample', ['ngAnimate']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.items = ['settings', 'home', 'other']; + $scope.selection = $scope.items[0]; + }]); </file> <file name="animations.css"> .animate-switch-container { @@ -20590,11 +25053,11 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { expect(switchElem.getText()).toMatch(/Settings Div/); }); it('should change to home', function() { - select.element.all(by.css('option')).get(1).click(); + select.all(by.css('option')).get(1).click(); expect(switchElem.getText()).toMatch(/Home Span/); }); it('should select default', function() { - select.element.all(by.css('option')).get(2).click(); + select.all(by.css('option')).get(2).click(); expect(switchElem.getText()).toMatch(/default/); }); </file> @@ -20611,47 +25074,41 @@ var ngSwitchDirective = ['$animate', function($animate) { }], link: function(scope, element, attr, ngSwitchController) { var watchExpr = attr.ngSwitch || attr.on, - selectedTranscludes, - selectedElements, - previousElements, + selectedTranscludes = [], + selectedElements = [], + previousLeaveAnimations = [], selectedScopes = []; + var spliceFactory = function(array, index) { + return function() { array.splice(index, 1); }; + }; + scope.$watch(watchExpr, function ngSwitchWatchAction(value) { - var i, ii = selectedScopes.length; - if(ii > 0) { - if(previousElements) { - for (i = 0; i < ii; i++) { - previousElements[i].remove(); - } - previousElements = null; - } + var i, ii; + for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { + $animate.cancel(previousLeaveAnimations[i]); + } + previousLeaveAnimations.length = 0; - previousElements = []; - for (i= 0; i<ii; i++) { - var selected = selectedElements[i]; - selectedScopes[i].$destroy(); - previousElements[i] = selected; - $animate.leave(selected, function() { - previousElements.splice(i, 1); - if(previousElements.length === 0) { - previousElements = null; - } - }); - } + for (i = 0, ii = selectedScopes.length; i < ii; ++i) { + var selected = getBlockNodes(selectedElements[i].clone); + selectedScopes[i].$destroy(); + var promise = previousLeaveAnimations[i] = $animate.leave(selected); + promise.then(spliceFactory(previousLeaveAnimations, i)); } - selectedElements = []; - selectedScopes = []; + selectedElements.length = 0; + selectedScopes.length = 0; if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) { - scope.$eval(attr.change); forEach(selectedTranscludes, function(selectedTransclude) { - var selectedScope = scope.$new(); - selectedScopes.push(selectedScope); - selectedTransclude.transclude(selectedScope, function(caseElement) { + selectedTransclude.transclude(function(caseElement, selectedScope) { + selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; + caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + var block = { clone: caseElement }; - selectedElements.push(caseElement); + selectedElements.push(block); $animate.enter(caseElement, anchor.parent(), anchor); }); }); @@ -20663,8 +25120,9 @@ var ngSwitchDirective = ['$animate', function($animate) { var ngSwitchWhenDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attrs, ctrl, $transclude) { ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); @@ -20673,8 +25131,9 @@ var ngSwitchWhenDirective = ngDirective({ var ngSwitchDefaultDirective = ngDirective({ transclude: 'element', - priority: 800, + priority: 1200, require: '^ngSwitch', + multiElement: true, link: function(scope, element, attr, ctrl, $transclude) { ctrl.cases['?'] = (ctrl.cases['?'] || []); ctrl.cases['?'].push({ transclude: $transclude, element: element }); @@ -20684,7 +25143,7 @@ var ngSwitchDefaultDirective = ngDirective({ /** * @ngdoc directive * @name ngTransclude - * @restrict AC + * @restrict EAC * * @description * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion. @@ -20694,15 +25153,10 @@ var ngSwitchDefaultDirective = ngDirective({ * @element ANY * * @example - <example module="transclude"> + <example module="transcludeExample"> <file name="index.html"> <script> - function Ctrl($scope) { - $scope.title = 'Lorem Ipsum'; - $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - } - - angular.module('transclude', []) + angular.module('transcludeExample', []) .directive('pane', function(){ return { restrict: 'E', @@ -20710,13 +25164,17 @@ var ngSwitchDefaultDirective = ngDirective({ scope: { title:'@' }, template: '<div style="border: 1px solid black;">' + '<div style="background-color: gray">{{title}}</div>' + - '<div ng-transclude></div>' + + '<ng-transclude></ng-transclude>' + '</div>' }; - }); + }) + .controller('ExampleController', ['$scope', function($scope) { + $scope.title = 'Lorem Ipsum'; + $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + }]); </script> - <div ng-controller="Ctrl"> - <input ng-model="title"><br> + <div ng-controller="ExampleController"> + <input ng-model="title"> <br/> <textarea ng-model="text"></textarea> <br/> <pane title="{{title}}">{{text}}</pane> </div> @@ -20737,6 +25195,7 @@ var ngSwitchDefaultDirective = ngDirective({ * */ var ngTranscludeDirective = ngDirective({ + restrict: 'EAC', link: function($scope, $element, $attrs, controller, $transclude) { if (!$transclude) { throw minErr('ngTransclude')('orphan', @@ -20793,7 +25252,6 @@ var scriptDirective = ['$templateCache', function($templateCache) { compile: function(element, attr) { if (attr.type == 'text/ng-template') { var templateUrl = attr.id, - // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent text = element[0].text; $templateCache.put(templateUrl, text); @@ -20817,26 +25275,49 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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, the `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 `select as`. `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. * - * <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> - * * 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:** `ngOptions` provides an iterator facility for the `<option>` element which should be used instead - * of {@link ng.directive:ngRepeat ngRepeat} when you want the - * `select` model to be bound to a non-string value. This is because an option element can only - * be bound to string values at present. + * **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> * + * ## `select as` + * + * Using `select as` will bind the result of the `select as` 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` with `track by` + * + * Using `select as` together with `track by` is not recommended. Reasoning: + * + * - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"> + * values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItem'}}], + * $scope.selected = {name: 'aSubItem'}; + * - track by is always applied to `value`, with the purpose of preserving the selection, + * (to `item` in this case) + * - to calculate whether an item is selected we do the following: + * 1. apply `track by` to the values in the array, e.g. + * 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}`. + * * @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. @@ -20848,8 +25329,10 @@ var ngOptionsMinErr = minErr('ngOptions'); * * for array data sources: * * `label` **`for`** `value` **`in`** `array` * * `select` **`as`** `label` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr` + * * `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` @@ -20871,24 +25354,26 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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`). + * `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> + <example module="selectExample"> <file name="index.html"> <script> - function MyCntrl($scope) { - $scope.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - $scope.color = $scope.colors[2]; // red - } + 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="MyCntrl"> + <div ng-controller="ExampleController"> <ul> <li ng-repeat="color in colors"> Name: <input ng-model="color.name"> @@ -20900,43 +25385,47 @@ var ngOptionsMinErr = minErr('ngOptions'); </ul> <hr/> Color (null not allowed): - <select ng-model="color" ng-options="c.name for c in colors"></select><br> + <select ng-model="myColor" ng-options="color.name for color in colors"></select><br> Color (null allowed): <span class="nullable"> - <select ng-model="color" ng-options="c.name for c in colors"> + <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="color" ng-options="c.name group by c.shade for c in colors"> + <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors"> </select><br/> - Select <a href ng-click="color={name:'not in list'}">bogus</a>.<br> + Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br> <hr/> - Currently selected: {{ {selected_color:color} }} + Currently selected: {{ {selected_color:myColor} }} <div style="border:solid 1px black; height:20px" - ng-style="{'background-color':color.name}"> + 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:color}')).getText()).toMatch('red'); - element.all(by.select('color')).first().click(); - element.all(by.css('select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('black'); - element(by.css('.nullable select[ng-model="color"]')).click(); - element.all(by.css('.nullable select[ng-model="color"] option')).first().click(); - expect(element(by.binding('{selected_color:color}')).getText()).toMatch('null'); + 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({ terminal: true }); +var ngOptionsDirective = valueFn({ + restrict: 'A', + terminal: true +}); + // jshint maxlen: false var selectDirective = ['$compile', '$parse', function($compile, $parse) { //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888 @@ -20965,7 +25454,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { }; - self.addOption = function(value) { + self.addOption = function(value, element) { assertNotHasOwnProperty(value, '"option value"'); optionsMap[value] = true; @@ -20973,13 +25462,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { $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) { + if (ngModelCtrl.$viewValue === value) { this.renderUnknownOption(value); } } @@ -21015,6 +25510,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { 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')), @@ -21022,7 +25518,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { unknownOption = optionTemplate.clone(); // find "null" option - for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) { + for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) { if (children[i].value === '') { emptyOption = nullOption = children.eq(i); break; @@ -21085,7 +25581,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // we need to work of an array, so we need to see if anything was inserted/removed scope.$watch(function selectMultipleWatch() { if (!equals(lastView, ctrl.$viewValue)) { - lastView = copy(ctrl.$viewValue); + lastView = shallowCopy(ctrl.$viewValue); ctrl.$render(); } }); @@ -21116,17 +25612,22 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { 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:''}]]; + 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 @@ -21144,154 +25645,202 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // clear contents, we'll add what's needed based on the model selectElement.empty(); - selectElement.on('change', function() { - scope.$apply(function() { - var optionGroup, - collection = valuesFn(scope) || [], - locals = {}, - key, value, optionElement, index, groupIndex, length, groupLength, trackIndex; + 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) { - value = []; - for (groupIndex = 0, groupLength = optionGroupsCache.length; - groupIndex < groupLength; - groupIndex++) { - // list of options for that group. (first item has the parent) - optionGroup = optionGroupsCache[groupIndex]; - - for(index = 1, length = optionGroup.length; index < length; index++) { - if ((optionElement = optionGroup[index].element)[0].selected) { - key = optionElement.val(); - if (keyName) locals[keyName] = key; - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) break; - } - } else { - locals[valueName] = collection[key]; - } - value.push(valueFn(scope, locals)); - } - } - } + viewValue = []; + forEach(selectElement.val(), function(selectedKey) { + selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey; + viewValue.push(getViewValue(selectedKey, collection[selectedKey])); + }); } else { - key = selectElement.val(); - if (key == '?') { - value = undefined; - } else if (key === ''){ - value = null; - } else { - if (trackFn) { - for (trackIndex = 0; trackIndex < collection.length; trackIndex++) { - locals[valueName] = collection[trackIndex]; - if (trackFn(scope, locals) == key) { - value = valueFn(scope, locals); - break; - } - } - } else { - locals[valueName] = collection[key]; - if (keyName) locals[keyName] = key; - value = valueFn(scope, locals); - } + 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]); } - // Update the null option's selected property here so $render cleans it up correctly - if (optionGroupsCache[0].length > 1) { - if (optionGroupsCache[0][1].id !== key) { - optionGroupsCache[0][1].selected = false; - } + } + } + 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); } - ctrl.$setViewValue(value); - }); - }); + } else if (trackFn) { + viewValue = callExpression(trackFn, null, viewValue); + } - ctrl.$render = render; + 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; + } + } - // TODO(vojta): can't we optimize this ? - scope.$watch(render); + /** + * 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() { - // Temporary location for the option groups before we render them + renderScheduled = false; + + // Temporary location for the option groups before we render them var optionGroups = {'':[]}, optionGroupNames = [''], optionGroupName, optionGroup, option, existingParent, existingOptions, existingOption, - modelValue = ctrl.$modelValue, + viewValue = ctrl.$viewValue, values = valuesFn(scope) || [], keys = keyName ? sortedKeys(values) : values, key, + value, groupLength, length, groupIndex, index, - locals = {}, + labelMap = {}, selected, - selectedSet = false, // nothing is selected yet + isSelected = createIsSelectedFn(viewValue), + anySelected = false, lastElement, element, - label; + label, + optionId; - if (multiple) { - if (trackFn && isArray(modelValue)) { - selectedSet = new HashMap([]); - for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) { - locals[valueName] = modelValue[trackIndex]; - selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]); - } - } else { - selectedSet = new HashMap(modelValue); - } - } + 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; - locals[keyName] = key; + if (key.charAt(0) === '$') continue; } + value = values[key]; - locals[valueName] = values[key]; - - optionGroupName = groupByFn(scope, locals) || ''; + optionGroupName = callExpression(groupByFn, key, value) || ''; if (!(optionGroup = optionGroups[optionGroupName])) { optionGroup = optionGroups[optionGroupName] = []; optionGroupNames.push(optionGroupName); } - if (multiple) { - selected = isDefined( - selectedSet.remove(trackFn ? trackFn(scope, locals) : valueFn(scope, locals)) - ); - } else { - if (trackFn) { - var modelCast = {}; - modelCast[valueName] = modelValue; - selected = trackFn(scope, modelCast) === trackFn(scope, locals); - } else { - selected = modelValue === valueFn(scope, locals); - } - selectedSet = selectedSet || selected; // see if at least one item is selected - } - label = displayFn(scope, locals); // what will be seen by the user + + 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: trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index), + id: optionId, label: label, selected: selected // determine if we should be selected }); } if (!multiple) { - if (nullOption || modelValue === null) { + if (nullOption || viewValue === null) { // insert null option if we have a placeholder, or the model is null - optionGroups[''].unshift({id:'', label:'', selected:!selectedSet}); - } else if (!selectedSet) { + 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}); } @@ -21327,20 +25876,29 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } lastElement = null; // start at the beginning - for(index = 0, length = optionGroup.length; index < length; index++) { + for (index = 0, length = optionGroup.length; index < length; index++) { option = optionGroup[index]; - if ((existingOption = existingOptions[index+1])) { + 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 (existingOption.selected !== option.selected) { + 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 @@ -21355,7 +25913,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { // 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); } @@ -21365,6 +25925,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { id: option.id, selected: option.selected }); + updateLabelMap(labelMap, option.label, true); if (lastElement) { lastElement.after(element); } else { @@ -21375,14 +25936,28 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) { } // remove any excessive OPTIONs in a group index++; // increment since the existingOptions[0] is parent element not OPTION - while(existingOptions.length > index) { - existingOptions.pop().element.remove(); + 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) { - optionGroupsCache.pop()[0].element.remove(); + 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); + } + }); } } } @@ -21406,28 +25981,26 @@ var optionDirective = ['$interpolate', function($interpolate) { } } - return function (scope, element, attr) { + return function(scope, element, attr) { var selectCtrlName = '$selectController', parent = element.parent(), selectCtrl = parent.data(selectCtrlName) || parent.parent().data(selectCtrlName); // in case we are in optgroup - if (selectCtrl && selectCtrl.databound) { - // For some reason Opera defaults to true and if not overridden this messes up the repeater. - // We don't want the view to drive the initialization of the model anyway. - element.prop('selected', false); - } else { + if (!selectCtrl || !selectCtrl.databound) { selectCtrl = nullSelectCtrl; } if (interpolateFn) { scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) { attr.$set('value', newVal); - if (newVal !== oldVal) selectCtrl.removeOption(oldVal); - selectCtrl.addOption(newVal); + if (oldVal !== newVal) { + selectCtrl.removeOption(oldVal); + } + selectCtrl.addOption(newVal, element); }); } else { - selectCtrl.addOption(attr.value); + selectCtrl.addOption(attr.value, element); } element.on('$destroy', function() { @@ -21440,7 +26013,7 @@ var optionDirective = ['$interpolate', function($interpolate) { var styleDirective = valueFn({ restrict: 'E', - terminal: true + terminal: false }); if (window.angular.bootstrap) { @@ -21449,7 +26022,7 @@ var styleDirective = valueFn({ return; } - //try to bind to jquery now so that one can write angular.element().read() + //try to bind to jquery now so that one can write jqLite(document).ready() //but we will rebind on bootstrap again. bindJQuery(); @@ -21461,4 +26034,4 @@ var styleDirective = valueFn({ })(window, document); -!angular.$$csp() && angular.element(document).find('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{display:none !important;}ng\\:form{display:block;}.ng-animate-block-transitions{transition:0s all!important;-webkit-transition:0s all!important;}</style>');
\ No newline at end of file +!window.angular.$$csp() && window.angular.element(document).find('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 diff --git a/xstatic/pkg/angular/data/errors.json b/xstatic/pkg/angular/data/errors.json index 4d3b96f..3f10e70 100644 --- a/xstatic/pkg/angular/data/errors.json +++ b/xstatic/pkg/angular/data/errors.json @@ -1 +1 @@ -{"id":"ng","generated":"Thu Apr 03 2014 15:18:46 GMT-0700 (PDT)","errors":{"$cacheFactory":{"iid":"CacheId '{0}' is already taken!"},"ngModel":{"nonassign":"Expression '{0}' is non-assignable. Element: {1}"},"$sce":{"iequirks":"Strict Contextual Escaping does not support Internet Explorer version < 9 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.","insecurl":"Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}","icontext":"Attempted to trust a value in invalid context. Context: {0}; Value: {1}","imatcher":"Matchers may only be \"self\", string patterns or RegExp objects","iwcard":"Illegal sequence *** in string matcher. String: {0}","itype":"Attempted to trust a non-string value in a content requiring a string: Context: {0}","unsafe":"Attempting to use an unsafe value in a safe context."},"$controller":{"noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$compile":{"nodomevents":"Interpolations for HTML DOM event attributes are disallowed. Please use the ng- versions (such as ng-click instead of onclick) instead.","multidir":"Multiple directives [{0}, {1}] asking for {2} on: {3}","nonassign":"Expression '{0}' used with directive '{1}' is non-assignable!","tplrt":"Template for directive '{0}' must have exactly one root element. {1}","selmulti":"Binding to the 'multiple' attribute is not supported. Element: {0}","tpload":"Failed to load template: {0}","iscp":"Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}","ctreq":"Controller '{0}', required by directive '{1}', can't be found!","uterdir":"Unterminated attribute, found '{0}' but no matching '{1}' found."},"$injector":{"modulerr":"Failed to instantiate module {0} due to:\n{1}","unpr":"Unknown provider: {0}","itkn":"Incorrect injection token! Expected service name as string, got {0}","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.","pget":"Provider '{0}' must define $get factory method."},"$rootScope":{"inprog":"{0} already in progress","infdig":"{0} $digest() iterations reached. Aborting!\nWatchers fired in the last 5 iterations: {1}"},"ngPattern":{"noregexp":"Expected {0} to be a RegExp but was {1}. Element: {2}"},"$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}"},"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"},"ngOptions":{"iexp":"Expected expression in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_' but got '{0}'. Element: {1}"},"ngRepeat":{"iidexp":"'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.","dupes":"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}","iexp":"Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'."},"ng":{"areq":"Argument '{0}' is {1}","cpws":"Can't copy! Making copies of Window or Scope instances is not supported.","badname":"hasOwnProperty is not a valid {0} name","btstrpd":"App Already Bootstrapped with this Element '{0}'","cpi":"Can't copy! Source and destination are identical."},"$animate":{"notcsel":"Expecting class selector starting with '.' got '{0}'."},"ngTransclude":{"orphan":"Illegal use of ngTransclude directive in the template! No parent directive that requires a transclusion found. Element: {0}"},"$parse":{"isecfld":"Referencing \"constructor\" field in Angular expressions is disallowed! Expression: {0}","syntax":"Syntax Error: Token '{0}' {1} at column {2} of the expression [{3}] starting at [{4}].","isecdom":"Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}","lexerr":"Lexer Error: {0} at column{1} in expression [{2}].","ueoe":"Unexpected end of expression: {0}","isecwindow":"Referencing the Window in Angular expressions is disallowed! Expression: {0}","isecfn":"Referencing Function in Angular expressions is disallowed! Expression: {0}"},"$httpBackend":{"noxhr":"This browser does not support XMLHttpRequest."},"$location":{"ipthprfx":"Invalid url \"{0}\", missing path prefix \"{1}\".","isrcharg":"The first argument of the `$location#search()` call must be a string or an object.","ihshprfx":"Invalid url \"{0}\", missing hash prefix \"{1}\"."},"$resource":{"badargs":"Expected up to 4 arguments [params, data, success, error], got {0} arguments","badmember":"Dotted member path \"@{0}\" is invalid.","badcfg":"Error in resource configuration. Expected response to contain an {0} but got an {1}","badname":"hasOwnProperty is not a valid parameter name."},"$sanitize":{"badparse":"The sanitizer was unable to parse the following block of html: {0}"}}}
\ No newline at end of file +{"id":"ng","generated":"Mon Dec 15 2014 13:55:17 GMT-0800 (PST)","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":{"noscp":"Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`."},"$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.","numfmt":"Expected `{0}` to be a number"},"$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":{"constexpr":"Expected constant expression for `{0}`, but saw `{1}`."},"$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}' ...}","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 diff --git a/xstatic/pkg/angular/data/version.json b/xstatic/pkg/angular/data/version.json index dbadcf3..f9c7b57 100644 --- a/xstatic/pkg/angular/data/version.json +++ b/xstatic/pkg/angular/data/version.json @@ -1 +1 @@ -{"raw":"v1.2.16","major":1,"minor":2,"patch":16,"prerelease":[],"build":[],"version":"1.2.16","codeName":"badger-enumeration","full":"1.2.16","cdn":{"raw":"v1.2.15","major":1,"minor":2,"patch":15,"prerelease":[],"build":[],"version":"1.2.15","isStable":true,"docsUrl":"http://code.angularjs.org/1.2.15/docs"}}
\ No newline at end of file +{"raw":"v1.3.7","major":1,"minor":3,"patch":7,"prerelease":[],"build":[],"version":"1.3.7","codeName":"leaky-obstruction","full":"1.3.7","branch":"v1.3.x","cdn":{"raw":"v1.3.6","major":1,"minor":3,"patch":6,"prerelease":[],"build":[],"version":"1.3.6","docsUrl":"http://code.angularjs.org/1.3.6/docs"}}
\ No newline at end of file diff --git a/xstatic/pkg/angular/data/version.txt b/xstatic/pkg/angular/data/version.txt index a96f385..8ed486a 100644 --- a/xstatic/pkg/angular/data/version.txt +++ b/xstatic/pkg/angular/data/version.txt @@ -1 +1 @@ -1.2.16
\ No newline at end of file +1.3.7
\ No newline at end of file |